refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
#include "game/movement_handler.hpp"
|
|
|
|
|
#include "game/game_handler.hpp"
|
|
|
|
|
#include "game/game_utils.hpp"
|
|
|
|
|
#include "game/packet_parsers.hpp"
|
|
|
|
|
#include "game/transport_manager.hpp"
|
|
|
|
|
#include "game/entity.hpp"
|
|
|
|
|
#include "network/world_socket.hpp"
|
|
|
|
|
#include "network/packet.hpp"
|
|
|
|
|
#include "core/coordinates.hpp"
|
|
|
|
|
#include "core/application.hpp"
|
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/dbc_layout.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <glm/gtx/quaternion.hpp>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <limits>
|
|
|
|
|
#include <zlib.h>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace game {
|
|
|
|
|
|
|
|
|
|
MovementHandler::MovementHandler(GameHandler& owner)
|
|
|
|
|
: owner_(owner), movementInfo(owner.movementInfo) {}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::registerOpcodes(DispatchTable& table) {
|
|
|
|
|
// Creature movement
|
|
|
|
|
table[Opcode::SMSG_MONSTER_MOVE] = [this](network::Packet& packet) { handleMonsterMove(packet); };
|
|
|
|
|
table[Opcode::SMSG_COMPRESSED_MOVES] = [this](network::Packet& packet) { handleCompressedMoves(packet); };
|
|
|
|
|
table[Opcode::SMSG_MONSTER_MOVE_TRANSPORT] = [this](network::Packet& packet) { handleMonsterMoveTransport(packet); };
|
|
|
|
|
|
|
|
|
|
// Spline move: consume-only (no state change)
|
|
|
|
|
for (auto op : { Opcode::SMSG_SPLINE_MOVE_FEATHER_FALL,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_GRAVITY_DISABLE,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_GRAVITY_ENABLE,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_LAND_WALK,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_NORMAL_FALL,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_ROOT,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_SET_HOVER }) {
|
|
|
|
|
table[op] = [this](network::Packet& packet) {
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.hasRemaining(1))
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
(void)packet.readPackedGuid();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spline move: synth flags (each opcode produces different flags)
|
|
|
|
|
{
|
|
|
|
|
auto makeSynthHandler = [this](uint32_t synthFlags) {
|
|
|
|
|
return [this, synthFlags](network::Packet& packet) {
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(1)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint64_t guid = packet.readPackedGuid();
|
|
|
|
|
if (guid == 0 || guid == owner_.playerGuid || !owner_.unitMoveFlagsCallback_) return;
|
|
|
|
|
owner_.unitMoveFlagsCallback_(guid, synthFlags);
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE] = makeSynthHandler(0x00000100u);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_MOVE_SET_RUN_MODE] = makeSynthHandler(0u);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_MOVE_SET_FLYING] = makeSynthHandler(0x01000000u | 0x00800000u);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_MOVE_START_SWIM] = makeSynthHandler(0x00200000u);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_MOVE_STOP_SWIM] = makeSynthHandler(0u);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 17:59:51 -07:00
|
|
|
// Spline speed: all opcodes share the same PackedGuid+float format, differing
|
|
|
|
|
// only in which member receives the value. Factory avoids 8 copy-pasted lambdas.
|
|
|
|
|
auto makeSplineSpeedHandler = [this](float MovementHandler::* member) {
|
|
|
|
|
return [this, member](network::Packet& packet) {
|
|
|
|
|
if (!packet.hasRemaining(5)) return;
|
|
|
|
|
uint64_t guid = packet.readPackedGuid();
|
|
|
|
|
if (!packet.hasRemaining(4)) return;
|
|
|
|
|
float speed = packet.readFloat();
|
|
|
|
|
if (guid == owner_.playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f)
|
|
|
|
|
this->*member = speed;
|
|
|
|
|
};
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
};
|
2026-03-29 17:59:51 -07:00
|
|
|
table[Opcode::SMSG_SPLINE_SET_RUN_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverRunSpeed_);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverRunBackSpeed_);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_SWIM_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverSwimSpeed_);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
// Force speed changes
|
|
|
|
|
table[Opcode::SMSG_FORCE_RUN_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceRunSpeedChange(packet); };
|
|
|
|
|
table[Opcode::SMSG_FORCE_MOVE_ROOT] = [this](network::Packet& packet) { handleForceMoveRootState(packet, true); };
|
|
|
|
|
table[Opcode::SMSG_FORCE_MOVE_UNROOT] = [this](network::Packet& packet) { handleForceMoveRootState(packet, false); };
|
|
|
|
|
table[Opcode::SMSG_FORCE_WALK_SPEED_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "WALK_SPEED", Opcode::CMSG_FORCE_WALK_SPEED_CHANGE_ACK, &serverWalkSpeed_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_RUN_BACK_SPEED_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "RUN_BACK_SPEED", Opcode::CMSG_FORCE_RUN_BACK_SPEED_CHANGE_ACK, &serverRunBackSpeed_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_SWIM_SPEED_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "SWIM_SPEED", Opcode::CMSG_FORCE_SWIM_SPEED_CHANGE_ACK, &serverSwimSpeed_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_SWIM_BACK_SPEED_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "SWIM_BACK_SPEED", Opcode::CMSG_FORCE_SWIM_BACK_SPEED_CHANGE_ACK, &serverSwimBackSpeed_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_FLIGHT_SPEED_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "FLIGHT_SPEED", Opcode::CMSG_FORCE_FLIGHT_SPEED_CHANGE_ACK, &serverFlightSpeed_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "FLIGHT_BACK_SPEED", Opcode::CMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE_ACK, &serverFlightBackSpeed_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_TURN_RATE_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "TURN_RATE", Opcode::CMSG_FORCE_TURN_RATE_CHANGE_ACK, &serverTurnRate_);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_FORCE_PITCH_RATE_CHANGE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "PITCH_RATE", Opcode::CMSG_FORCE_PITCH_RATE_CHANGE_ACK, &serverPitchRate_);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Movement flag toggles
|
|
|
|
|
table[Opcode::SMSG_MOVE_SET_CAN_FLY] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "SET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::CAN_FLY), true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_UNSET_CAN_FLY] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "UNSET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::CAN_FLY), false);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_FEATHER_FALL] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "FEATHER_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FEATHER_FALL), true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_WATER_WALK] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "WATER_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::WATER_WALK), true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_SET_HOVER] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "SET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::HOVER), true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_UNSET_HOVER] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "UNSET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::HOVER), false);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_KNOCK_BACK] = [this](network::Packet& packet) { handleMoveKnockBack(packet); };
|
|
|
|
|
|
|
|
|
|
// Teleport
|
|
|
|
|
for (auto op : { Opcode::MSG_MOVE_TELEPORT, Opcode::MSG_MOVE_TELEPORT_ACK }) {
|
|
|
|
|
table[op] = [this](network::Packet& packet) { handleTeleportAck(packet); };
|
|
|
|
|
}
|
|
|
|
|
table[Opcode::SMSG_NEW_WORLD] = [this](network::Packet& packet) { handleNewWorld(packet); };
|
|
|
|
|
|
|
|
|
|
// Taxi
|
|
|
|
|
table[Opcode::SMSG_SHOWTAXINODES] = [this](network::Packet& packet) { handleShowTaxiNodes(packet); };
|
|
|
|
|
table[Opcode::SMSG_ACTIVATETAXIREPLY] = [this](network::Packet& packet) { handleActivateTaxiReply(packet); };
|
|
|
|
|
|
|
|
|
|
// MSG_MOVE_* relay (other player movement)
|
|
|
|
|
for (auto op : { Opcode::MSG_MOVE_START_FORWARD, Opcode::MSG_MOVE_START_BACKWARD,
|
|
|
|
|
Opcode::MSG_MOVE_STOP, Opcode::MSG_MOVE_START_STRAFE_LEFT,
|
|
|
|
|
Opcode::MSG_MOVE_START_STRAFE_RIGHT, Opcode::MSG_MOVE_STOP_STRAFE,
|
|
|
|
|
Opcode::MSG_MOVE_JUMP, Opcode::MSG_MOVE_START_TURN_LEFT,
|
|
|
|
|
Opcode::MSG_MOVE_START_TURN_RIGHT, Opcode::MSG_MOVE_STOP_TURN,
|
|
|
|
|
Opcode::MSG_MOVE_SET_FACING, Opcode::MSG_MOVE_FALL_LAND,
|
|
|
|
|
Opcode::MSG_MOVE_HEARTBEAT, Opcode::MSG_MOVE_START_SWIM,
|
|
|
|
|
Opcode::MSG_MOVE_STOP_SWIM, Opcode::MSG_MOVE_SET_WALK_MODE,
|
|
|
|
|
Opcode::MSG_MOVE_SET_RUN_MODE, Opcode::MSG_MOVE_START_PITCH_UP,
|
|
|
|
|
Opcode::MSG_MOVE_START_PITCH_DOWN, Opcode::MSG_MOVE_STOP_PITCH,
|
|
|
|
|
Opcode::MSG_MOVE_START_ASCEND, Opcode::MSG_MOVE_STOP_ASCEND,
|
|
|
|
|
Opcode::MSG_MOVE_START_DESCEND, Opcode::MSG_MOVE_SET_PITCH,
|
|
|
|
|
Opcode::MSG_MOVE_GRAVITY_CHNG, Opcode::MSG_MOVE_UPDATE_CAN_FLY,
|
|
|
|
|
Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY,
|
|
|
|
|
Opcode::MSG_MOVE_ROOT, Opcode::MSG_MOVE_UNROOT }) {
|
|
|
|
|
table[op] = [this](network::Packet& packet) {
|
|
|
|
|
if (owner_.getState() == WorldState::IN_WORLD) handleOtherPlayerMovement(packet);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MSG_MOVE_SET_*_SPEED relay
|
|
|
|
|
for (auto op : { Opcode::MSG_MOVE_SET_RUN_SPEED, Opcode::MSG_MOVE_SET_RUN_BACK_SPEED,
|
|
|
|
|
Opcode::MSG_MOVE_SET_WALK_SPEED, Opcode::MSG_MOVE_SET_SWIM_SPEED,
|
|
|
|
|
Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED, Opcode::MSG_MOVE_SET_FLIGHT_SPEED,
|
|
|
|
|
Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED }) {
|
|
|
|
|
table[op] = [this](network::Packet& packet) {
|
|
|
|
|
if (owner_.getState() == WorldState::IN_WORLD) handleMoveSetSpeed(packet);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Client control & spline speed/flag changes ----
|
|
|
|
|
|
|
|
|
|
// Client control update
|
|
|
|
|
table[Opcode::SMSG_CLIENT_CONTROL_UPDATE] = [this](network::Packet& packet) {
|
|
|
|
|
handleClientControlUpdate(packet);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Spline move flag changes for other units (unroot/unset_hover/water_walk)
|
|
|
|
|
for (auto op : {Opcode::SMSG_SPLINE_MOVE_UNROOT,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_UNSET_HOVER,
|
|
|
|
|
Opcode::SMSG_SPLINE_MOVE_WATER_WALK}) {
|
|
|
|
|
table[op] = [this](network::Packet& packet) {
|
|
|
|
|
// Minimal parse: PackedGuid only — no animation-relevant state change.
|
|
|
|
|
if (packet.hasRemaining(1)) {
|
|
|
|
|
(void)packet.readPackedGuid();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table[Opcode::SMSG_SPLINE_MOVE_UNSET_FLYING] = [this](network::Packet& packet) {
|
|
|
|
|
// PackedGuid + synthesised move-flags=0 → clears flying animation.
|
|
|
|
|
if (!packet.hasRemaining(1)) return;
|
|
|
|
|
uint64_t guid = packet.readPackedGuid();
|
|
|
|
|
if (guid == 0 || guid == owner_.playerGuid || !owner_.unitMoveFlagsCallback_) return;
|
|
|
|
|
owner_.unitMoveFlagsCallback_(guid, 0u); // clear flying/CAN_FLY
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-29 17:59:51 -07:00
|
|
|
// Remaining spline speed opcodes — same factory as above.
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_FLIGHT_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverFlightSpeed_);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_FLIGHT_BACK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverFlightBackSpeed_);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverSwimBackSpeed_);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_WALK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverWalkSpeed_);
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_TURN_RATE] = makeSplineSpeedHandler(&MovementHandler::serverTurnRate_);
|
|
|
|
|
// Pitch rate not stored locally — consume packet to keep stream aligned.
|
|
|
|
|
table[Opcode::SMSG_SPLINE_SET_PITCH_RATE] = [](network::Packet& packet) { packet.skipAll(); };
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
// ---- Player movement flag changes (server-pushed) ----
|
|
|
|
|
table[Opcode::SMSG_MOVE_GRAVITY_DISABLE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "GRAVITY_DISABLE", Opcode::CMSG_MOVE_GRAVITY_DISABLE_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::LEVITATING), true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_GRAVITY_ENABLE] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "GRAVITY_ENABLE", Opcode::CMSG_MOVE_GRAVITY_ENABLE_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::LEVITATING), false);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_LAND_WALK] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "LAND_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::WATER_WALK), false);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_NORMAL_FALL] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "NORMAL_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FEATHER_FALL), false);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "SET_CAN_TRANSITION_SWIM_FLY",
|
|
|
|
|
Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_UNSET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "UNSET_CAN_TRANSITION_SWIM_FLY",
|
|
|
|
|
Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, false);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_SET_COLLISION_HGT] = [this](network::Packet& packet) {
|
|
|
|
|
handleMoveSetCollisionHeight(packet);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_SET_FLIGHT] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "SET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FLYING), true);
|
|
|
|
|
};
|
|
|
|
|
table[Opcode::SMSG_MOVE_UNSET_FLIGHT] = [this](network::Packet& packet) {
|
|
|
|
|
handleForceMoveFlagChange(packet, "UNSET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK,
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FLYING), false);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// handleClientControlUpdate
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleClientControlUpdate(network::Packet& packet) {
|
|
|
|
|
// Minimal parse: PackedGuid + uint8 allowMovement.
|
|
|
|
|
if (!packet.hasRemaining(2)) {
|
|
|
|
|
LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE too short: ", packet.getSize(), " bytes");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
uint8_t guidMask = packet.readUInt8();
|
|
|
|
|
size_t guidBytes = 0;
|
|
|
|
|
uint64_t controlGuid = 0;
|
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
|
|
|
if (guidMask & (1u << i)) ++guidBytes;
|
|
|
|
|
}
|
|
|
|
|
if (!packet.hasRemaining(guidBytes) + 1) {
|
|
|
|
|
LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE malformed (truncated packed guid)");
|
|
|
|
|
packet.skipAll();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
|
|
|
if (guidMask & (1u << i)) {
|
|
|
|
|
uint8_t b = packet.readUInt8();
|
|
|
|
|
controlGuid |= (static_cast<uint64_t>(b) << (i * 8));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
bool allowMovement = (packet.readUInt8() != 0);
|
|
|
|
|
if (controlGuid == 0 || controlGuid == owner_.playerGuid) {
|
|
|
|
|
bool changed = (serverMovementAllowed_ != allowMovement);
|
|
|
|
|
serverMovementAllowed_ = allowMovement;
|
|
|
|
|
if (changed && !allowMovement) {
|
|
|
|
|
// Force-stop local movement immediately when server revokes control.
|
|
|
|
|
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::FORWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::BACKWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_RIGHT));
|
|
|
|
|
owner_.sendMovement(Opcode::MSG_MOVE_STOP);
|
|
|
|
|
owner_.sendMovement(Opcode::MSG_MOVE_STOP_STRAFE);
|
|
|
|
|
owner_.sendMovement(Opcode::MSG_MOVE_STOP_TURN);
|
|
|
|
|
owner_.sendMovement(Opcode::MSG_MOVE_STOP_SWIM);
|
|
|
|
|
owner_.addSystemChatMessage("Movement disabled by server.");
|
|
|
|
|
owner_.fireAddonEvent("PLAYER_CONTROL_LOST", {});
|
|
|
|
|
} else if (changed && allowMovement) {
|
|
|
|
|
owner_.addSystemChatMessage("Movement re-enabled.");
|
|
|
|
|
owner_.fireAddonEvent("PLAYER_CONTROL_GAINED", {});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Movement Timestamp
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
uint32_t MovementHandler::nextMovementTimestampMs() {
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
uint64_t elapsed = static_cast<uint64_t>(
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - movementClockStart_).count()) + 1ULL;
|
|
|
|
|
if (elapsed > std::numeric_limits<uint32_t>::max()) {
|
|
|
|
|
movementClockStart_ = now;
|
|
|
|
|
elapsed = 1ULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t candidate = static_cast<uint32_t>(elapsed);
|
|
|
|
|
if (candidate <= lastMovementTimestampMs_) {
|
|
|
|
|
candidate = lastMovementTimestampMs_ + 1U;
|
|
|
|
|
if (candidate == 0) {
|
|
|
|
|
movementClockStart_ = now;
|
|
|
|
|
candidate = 1U;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastMovementTimestampMs_ = candidate;
|
|
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// sendMovement
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::sendMovement(Opcode opcode) {
|
|
|
|
|
if (owner_.state != WorldState::IN_WORLD) {
|
|
|
|
|
LOG_WARNING("Cannot send movement in state: ", (int)owner_.state);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Block manual movement while taxi is active/mounted, but always allow
|
|
|
|
|
// stop/heartbeat opcodes so stuck states can be recovered.
|
|
|
|
|
bool taxiAllowed =
|
|
|
|
|
(opcode == Opcode::MSG_MOVE_HEARTBEAT) ||
|
|
|
|
|
(opcode == Opcode::MSG_MOVE_STOP) ||
|
|
|
|
|
(opcode == Opcode::MSG_MOVE_STOP_STRAFE) ||
|
|
|
|
|
(opcode == Opcode::MSG_MOVE_STOP_TURN) ||
|
|
|
|
|
(opcode == Opcode::MSG_MOVE_STOP_SWIM);
|
|
|
|
|
if (!serverMovementAllowed_ && !taxiAllowed) return;
|
|
|
|
|
if ((onTaxiFlight_ || taxiMountActive_) && !taxiAllowed) return;
|
|
|
|
|
if (owner_.resurrectPending_ && !taxiAllowed) return;
|
|
|
|
|
|
|
|
|
|
// Always send a strictly increasing non-zero client movement clock value.
|
|
|
|
|
const uint32_t movementTime = nextMovementTimestampMs();
|
|
|
|
|
movementInfo.time = movementTime;
|
|
|
|
|
|
|
|
|
|
if (opcode == Opcode::MSG_MOVE_SET_FACING &&
|
|
|
|
|
(isClassicLikeExpansion() || isActiveExpansion("tbc"))) {
|
|
|
|
|
const float facingDelta = core::coords::normalizeAngleRad(
|
|
|
|
|
movementInfo.orientation - lastFacingSentOrientation_);
|
|
|
|
|
const uint32_t sinceLastFacingMs =
|
|
|
|
|
lastFacingSendTimeMs_ != 0 && movementTime >= lastFacingSendTimeMs_
|
|
|
|
|
? (movementTime - lastFacingSendTimeMs_)
|
|
|
|
|
: std::numeric_limits<uint32_t>::max();
|
|
|
|
|
if (std::abs(facingDelta) < 0.02f && sinceLastFacingMs < 200U) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track movement state transition for PLAYER_STARTED/STOPPED_MOVING events
|
|
|
|
|
const uint32_t kMoveMask = static_cast<uint32_t>(MovementFlags::FORWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::BACKWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
|
|
|
|
|
const bool wasMoving = (movementInfo.flags & kMoveMask) != 0;
|
|
|
|
|
|
|
|
|
|
// Cancel any timed (non-channeled) cast the moment the player starts moving.
|
2026-03-28 11:35:10 +03:00
|
|
|
if (owner_.isCasting() && !owner_.isChanneling()) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
const bool isPositionalMove =
|
|
|
|
|
opcode == Opcode::MSG_MOVE_START_FORWARD ||
|
|
|
|
|
opcode == Opcode::MSG_MOVE_START_BACKWARD ||
|
|
|
|
|
opcode == Opcode::MSG_MOVE_START_STRAFE_LEFT ||
|
|
|
|
|
opcode == Opcode::MSG_MOVE_START_STRAFE_RIGHT ||
|
|
|
|
|
opcode == Opcode::MSG_MOVE_JUMP;
|
|
|
|
|
if (isPositionalMove) {
|
|
|
|
|
owner_.cancelCast();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update movement flags based on opcode
|
|
|
|
|
switch (opcode) {
|
|
|
|
|
case Opcode::MSG_MOVE_START_FORWARD:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FORWARD);
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_BACKWARD:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::BACKWARD);
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_STOP:
|
|
|
|
|
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::FORWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::BACKWARD));
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_STRAFE_LEFT:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::STRAFE_LEFT);
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_STRAFE_RIGHT:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_STOP_STRAFE:
|
|
|
|
|
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT));
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_JUMP:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FALLING);
|
|
|
|
|
isFalling_ = true;
|
|
|
|
|
fallStartMs_ = movementInfo.time;
|
|
|
|
|
movementInfo.fallTime = 0;
|
|
|
|
|
movementInfo.jumpVelocity = 7.96f;
|
|
|
|
|
{
|
|
|
|
|
const float facingRad = movementInfo.orientation;
|
|
|
|
|
movementInfo.jumpCosAngle = std::cos(facingRad);
|
|
|
|
|
movementInfo.jumpSinAngle = std::sin(facingRad);
|
|
|
|
|
const uint32_t horizFlags =
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FORWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::BACKWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
|
|
|
|
|
const bool movingHoriz = (movementInfo.flags & horizFlags) != 0;
|
|
|
|
|
if (movingHoriz) {
|
|
|
|
|
const bool isWalking = (movementInfo.flags & static_cast<uint32_t>(MovementFlags::WALKING)) != 0;
|
|
|
|
|
movementInfo.jumpXYSpeed = isWalking ? 2.5f : (serverRunSpeed_ > 0.0f ? serverRunSpeed_ : 7.0f);
|
|
|
|
|
} else {
|
|
|
|
|
movementInfo.jumpXYSpeed = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_TURN_LEFT:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_LEFT);
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_TURN_RIGHT:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_RIGHT);
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_STOP_TURN:
|
|
|
|
|
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_RIGHT));
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_FALL_LAND:
|
|
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::FALLING);
|
|
|
|
|
isFalling_ = false;
|
|
|
|
|
fallStartMs_ = 0;
|
|
|
|
|
movementInfo.fallTime = 0;
|
|
|
|
|
movementInfo.jumpVelocity = 0.0f;
|
|
|
|
|
movementInfo.jumpSinAngle = 0.0f;
|
|
|
|
|
movementInfo.jumpCosAngle = 0.0f;
|
|
|
|
|
movementInfo.jumpXYSpeed = 0.0f;
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_HEARTBEAT:
|
|
|
|
|
timeSinceLastMoveHeartbeat_ = 0.0f;
|
|
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_ASCEND:
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ASCENDING);
|
2026-03-29 18:28:49 -07:00
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::DESCENDING);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_STOP_ASCEND:
|
|
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ASCENDING);
|
2026-03-29 18:28:49 -07:00
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::DESCENDING);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
break;
|
|
|
|
|
case Opcode::MSG_MOVE_START_DESCEND:
|
2026-03-29 18:28:49 -07:00
|
|
|
// Must set DESCENDING so outgoing movement packets carry the correct
|
|
|
|
|
// flag during flight descent. Only clearing ASCENDING left the flag
|
|
|
|
|
// field ambiguous (neither ascending nor descending).
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ASCENDING);
|
2026-03-29 18:28:49 -07:00
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::DESCENDING);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fire PLAYER_STARTED/STOPPED_MOVING on movement state transitions
|
|
|
|
|
{
|
|
|
|
|
const bool isMoving = (movementInfo.flags & kMoveMask) != 0;
|
|
|
|
|
if (isMoving && !wasMoving && owner_.addonEventCallback_)
|
|
|
|
|
owner_.addonEventCallback_("PLAYER_STARTED_MOVING", {});
|
|
|
|
|
else if (!isMoving && wasMoving && owner_.addonEventCallback_)
|
|
|
|
|
owner_.addonEventCallback_("PLAYER_STOPPED_MOVING", {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opcode == Opcode::MSG_MOVE_SET_FACING) {
|
|
|
|
|
lastFacingSendTimeMs_ = movementInfo.time;
|
|
|
|
|
lastFacingSentOrientation_ = movementInfo.orientation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep fallTime current
|
|
|
|
|
if (isFalling_ && movementInfo.hasFlag(MovementFlags::FALLING)) {
|
|
|
|
|
uint32_t elapsed = (movementInfo.time >= fallStartMs_)
|
|
|
|
|
? (movementInfo.time - fallStartMs_)
|
|
|
|
|
: 0u;
|
|
|
|
|
movementInfo.fallTime = elapsed;
|
|
|
|
|
} else if (!movementInfo.hasFlag(MovementFlags::FALLING)) {
|
|
|
|
|
if (isFalling_) {
|
|
|
|
|
isFalling_ = false;
|
|
|
|
|
fallStartMs_ = 0;
|
|
|
|
|
}
|
|
|
|
|
movementInfo.fallTime = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (onTaxiFlight_ || taxiMountActive_ || taxiActivatePending_ || taxiClientActive_) {
|
|
|
|
|
sanitizeMovementForTaxi();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool includeTransportInWire = owner_.isOnTransport();
|
|
|
|
|
if (includeTransportInWire && owner_.transportManager_) {
|
|
|
|
|
if (auto* tr = owner_.transportManager_->getTransport(owner_.playerTransportGuid_); tr && tr->isM2) {
|
|
|
|
|
includeTransportInWire = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add transport data if player is on a server-recognized transport
|
|
|
|
|
if (includeTransportInWire) {
|
fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.
WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.
Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00
|
|
|
bool transportResolved = false;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (owner_.transportManager_) {
|
2026-04-05 16:01:14 -07:00
|
|
|
auto* tr = owner_.transportManager_->getTransport(owner_.playerTransportGuid_);
|
|
|
|
|
if (tr) {
|
fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.
WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.
Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00
|
|
|
transportResolved = true;
|
2026-04-05 16:01:14 -07:00
|
|
|
glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition(owner_.playerTransportGuid_, owner_.playerTransportOffset_);
|
|
|
|
|
movementInfo.x = composed.x;
|
|
|
|
|
movementInfo.y = composed.y;
|
|
|
|
|
movementInfo.z = composed.z;
|
|
|
|
|
}
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
}
|
fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.
WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.
Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00
|
|
|
if (!transportResolved) {
|
|
|
|
|
// Transport not tracked — don't send ONTRANSPORT to the server.
|
|
|
|
|
// Sending stale transport GUID + local offset causes the server to
|
|
|
|
|
// compute a bad world position and teleport us to map origin.
|
|
|
|
|
LOG_WARNING("sendMovement: transport 0x", std::hex, owner_.playerTransportGuid_,
|
|
|
|
|
std::dec, " not found — clearing transport state");
|
|
|
|
|
includeTransportInWire = false;
|
|
|
|
|
owner_.clearPlayerTransport();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (includeTransportInWire) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
|
|
|
|
movementInfo.transportGuid = owner_.playerTransportGuid_;
|
|
|
|
|
movementInfo.transportX = owner_.playerTransportOffset_.x;
|
|
|
|
|
movementInfo.transportY = owner_.playerTransportOffset_.y;
|
|
|
|
|
movementInfo.transportZ = owner_.playerTransportOffset_.z;
|
|
|
|
|
movementInfo.transportTime = movementInfo.time;
|
|
|
|
|
movementInfo.transportSeat = -1;
|
|
|
|
|
movementInfo.transportTime2 = movementInfo.time;
|
|
|
|
|
|
|
|
|
|
float transportYawCanonical = 0.0f;
|
|
|
|
|
if (owner_.transportManager_) {
|
|
|
|
|
if (auto* tr = owner_.transportManager_->getTransport(owner_.playerTransportGuid_); tr) {
|
|
|
|
|
if (tr->hasServerYaw) {
|
|
|
|
|
transportYawCanonical = tr->serverYaw;
|
|
|
|
|
} else {
|
|
|
|
|
transportYawCanonical = glm::eulerAngles(tr->rotation).z;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
movementInfo.transportO =
|
|
|
|
|
core::coords::normalizeAngleRad(movementInfo.orientation - transportYawCanonical);
|
|
|
|
|
} else {
|
|
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
|
|
|
|
movementInfo.transportGuid = 0;
|
|
|
|
|
movementInfo.transportSeat = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && isClassicLikeExpansion()) {
|
|
|
|
|
const uint32_t locomotionFlags =
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FORWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::BACKWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_RIGHT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::ASCENDING) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FALLING) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FALLINGFAR) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::SWIMMING);
|
|
|
|
|
const bool stationaryIdle =
|
|
|
|
|
!onTaxiFlight_ &&
|
|
|
|
|
!taxiMountActive_ &&
|
|
|
|
|
!taxiActivatePending_ &&
|
|
|
|
|
!taxiClientActive_ &&
|
|
|
|
|
!includeTransportInWire &&
|
|
|
|
|
(movementInfo.flags & locomotionFlags) == 0;
|
|
|
|
|
const uint32_t sinceLastHeartbeatMs =
|
|
|
|
|
lastHeartbeatSendTimeMs_ != 0 && movementTime >= lastHeartbeatSendTimeMs_
|
|
|
|
|
? (movementTime - lastHeartbeatSendTimeMs_)
|
|
|
|
|
: std::numeric_limits<uint32_t>::max();
|
|
|
|
|
const bool unchangedState =
|
|
|
|
|
std::abs(movementInfo.x - lastHeartbeatX_) < 0.01f &&
|
|
|
|
|
std::abs(movementInfo.y - lastHeartbeatY_) < 0.01f &&
|
|
|
|
|
std::abs(movementInfo.z - lastHeartbeatZ_) < 0.01f &&
|
|
|
|
|
movementInfo.flags == lastHeartbeatFlags_ &&
|
|
|
|
|
movementInfo.transportGuid == lastHeartbeatTransportGuid_;
|
|
|
|
|
if (stationaryIdle && unchangedState && sinceLastHeartbeatMs < 1500U) {
|
|
|
|
|
timeSinceLastMoveHeartbeat_ = 0.0f;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const uint32_t sinceLastNonHeartbeatMoveMs =
|
|
|
|
|
lastNonHeartbeatMoveSendTimeMs_ != 0 && movementTime >= lastNonHeartbeatMoveSendTimeMs_
|
|
|
|
|
? (movementTime - lastNonHeartbeatMoveSendTimeMs_)
|
|
|
|
|
: std::numeric_limits<uint32_t>::max();
|
|
|
|
|
if (sinceLastNonHeartbeatMoveMs < 350U) {
|
|
|
|
|
timeSinceLastMoveHeartbeat_ = 0.0f;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
|
|
|
|
|
wireOpcode(opcode), std::dec,
|
|
|
|
|
(includeTransportInWire ? " ONTRANSPORT" : ""));
|
|
|
|
|
|
fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.
WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.
Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00
|
|
|
// Detect near-origin position on Eastern Kingdoms (map 0) — this would place
|
|
|
|
|
// the player near Alterac Mountains and is almost certainly a bug.
|
|
|
|
|
if (owner_.currentMapId_ == 0 &&
|
|
|
|
|
std::abs(movementInfo.x) < 500.0f && std::abs(movementInfo.y) < 500.0f) {
|
|
|
|
|
LOG_WARNING("sendMovement: position near map origin! canonical=(",
|
|
|
|
|
movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z,
|
|
|
|
|
") onTransport=", owner_.isOnTransport(),
|
|
|
|
|
" transportGuid=0x", std::hex, owner_.playerTransportGuid_, std::dec,
|
|
|
|
|
" flags=0x", std::hex, movementInfo.flags, std::dec);
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
// Convert canonical → server coordinates for the wire
|
|
|
|
|
MovementInfo wireInfo = movementInfo;
|
|
|
|
|
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wireInfo.x, wireInfo.y, wireInfo.z));
|
|
|
|
|
wireInfo.x = serverPos.x;
|
|
|
|
|
wireInfo.y = serverPos.y;
|
|
|
|
|
wireInfo.z = serverPos.z;
|
|
|
|
|
|
2026-03-29 18:28:49 -07:00
|
|
|
// Periodic position audit — DEBUG to avoid flooding production logs.
|
|
|
|
|
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 30 == 0) {
|
|
|
|
|
LOG_DEBUG("HEARTBEAT pos canonical=(", movementInfo.x, ",", movementInfo.y, ",", movementInfo.z,
|
|
|
|
|
") wire=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z, ")");
|
2026-03-28 16:02:36 -07:00
|
|
|
}
|
|
|
|
|
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
|
|
|
|
|
|
|
|
|
|
if (includeTransportInWire) {
|
|
|
|
|
glm::vec3 serverTransportPos = core::coords::canonicalToServer(
|
|
|
|
|
glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ));
|
|
|
|
|
wireInfo.transportX = serverTransportPos.x;
|
|
|
|
|
wireInfo.transportY = serverTransportPos.y;
|
|
|
|
|
wireInfo.transportZ = serverTransportPos.z;
|
|
|
|
|
wireInfo.transportO = core::coords::normalizeAngleRad(-wireInfo.transportO);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build and send movement packet (expansion-specific format)
|
|
|
|
|
auto packet = owner_.packetParsers_
|
|
|
|
|
? owner_.packetParsers_->buildMovementPacket(opcode, wireInfo, owner_.playerGuid)
|
|
|
|
|
: MovementPacket::build(opcode, wireInfo, owner_.playerGuid);
|
|
|
|
|
owner_.socket->send(packet);
|
|
|
|
|
|
|
|
|
|
if (opcode == Opcode::MSG_MOVE_HEARTBEAT) {
|
|
|
|
|
lastHeartbeatSendTimeMs_ = movementInfo.time;
|
|
|
|
|
lastHeartbeatX_ = movementInfo.x;
|
|
|
|
|
lastHeartbeatY_ = movementInfo.y;
|
|
|
|
|
lastHeartbeatZ_ = movementInfo.z;
|
|
|
|
|
lastHeartbeatFlags_ = movementInfo.flags;
|
|
|
|
|
lastHeartbeatTransportGuid_ = movementInfo.transportGuid;
|
|
|
|
|
} else {
|
|
|
|
|
lastNonHeartbeatMoveSendTimeMs_ = movementInfo.time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// sanitizeMovementForTaxi
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::sanitizeMovementForTaxi() {
|
|
|
|
|
constexpr uint32_t kClearTaxiFlags =
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FORWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::BACKWARD) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::TURN_RIGHT) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::PITCH_UP) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::PITCH_DOWN) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FALLING) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::FALLINGFAR) |
|
|
|
|
|
static_cast<uint32_t>(MovementFlags::SWIMMING);
|
|
|
|
|
|
|
|
|
|
movementInfo.flags &= ~kClearTaxiFlags;
|
|
|
|
|
movementInfo.fallTime = 0;
|
|
|
|
|
movementInfo.jumpVelocity = 0.0f;
|
|
|
|
|
movementInfo.jumpSinAngle = 0.0f;
|
|
|
|
|
movementInfo.jumpCosAngle = 0.0f;
|
|
|
|
|
movementInfo.jumpXYSpeed = 0.0f;
|
|
|
|
|
movementInfo.pitch = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// forceClearTaxiAndMovementState
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::forceClearTaxiAndMovementState() {
|
|
|
|
|
taxiActivatePending_ = false;
|
|
|
|
|
taxiActivateTimer_ = 0.0f;
|
|
|
|
|
taxiClientActive_ = false;
|
|
|
|
|
taxiClientPath_.clear();
|
|
|
|
|
taxiRecoverPending_ = false;
|
|
|
|
|
taxiStartGrace_ = 0.0f;
|
|
|
|
|
onTaxiFlight_ = false;
|
|
|
|
|
|
|
|
|
|
if (taxiMountActive_ && owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
taxiMountActive_ = false;
|
|
|
|
|
taxiMountDisplayId_ = 0;
|
|
|
|
|
owner_.currentMountDisplayId_ = 0;
|
|
|
|
|
owner_.vehicleId_ = 0;
|
2026-03-29 19:01:34 -07:00
|
|
|
// Death/resurrect state is intentionally NOT cleared here.
|
|
|
|
|
// Previously this method reset 10 death-related fields despite being named
|
|
|
|
|
// "forceClearTaxiAndMovementState", which could cancel pending resurrections
|
|
|
|
|
// or clear death state on taxi dismount. Death state is managed by
|
|
|
|
|
// entity_controller (markPlayerDead) and the resurrect packet handlers.
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
movementInfo.flags = 0;
|
|
|
|
|
movementInfo.flags2 = 0;
|
|
|
|
|
movementInfo.transportGuid = 0;
|
|
|
|
|
owner_.clearPlayerTransport();
|
|
|
|
|
|
|
|
|
|
if (owner_.socket && owner_.state == WorldState::IN_WORLD) {
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_STOP);
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_STOP_STRAFE);
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_STOP_TURN);
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_STOP_SWIM);
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Force-cleared taxi/movement state");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// setPosition / setOrientation
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::setPosition(float x, float y, float z) {
|
|
|
|
|
movementInfo.x = x;
|
|
|
|
|
movementInfo.y = y;
|
|
|
|
|
movementInfo.z = z;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::setOrientation(float orientation) {
|
|
|
|
|
movementInfo.orientation = orientation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// dismount
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::dismount() {
|
|
|
|
|
if (!owner_.socket) return;
|
|
|
|
|
uint32_t savedMountAura = owner_.mountAuraSpellId_;
|
|
|
|
|
if (owner_.currentMountDisplayId_ != 0 || taxiMountActive_) {
|
|
|
|
|
if (owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
owner_.currentMountDisplayId_ = 0;
|
|
|
|
|
taxiMountActive_ = false;
|
|
|
|
|
taxiMountDisplayId_ = 0;
|
|
|
|
|
owner_.mountAuraSpellId_ = 0;
|
|
|
|
|
LOG_INFO("Dismount: cleared local mount state");
|
|
|
|
|
}
|
|
|
|
|
uint16_t cancelMountWire = wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA);
|
|
|
|
|
if (cancelMountWire != 0xFFFF) {
|
|
|
|
|
network::Packet pkt(cancelMountWire);
|
|
|
|
|
owner_.socket->send(pkt);
|
|
|
|
|
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
|
|
|
|
|
} else if (savedMountAura != 0) {
|
|
|
|
|
auto pkt = CancelAuraPacket::build(savedMountAura);
|
|
|
|
|
owner_.socket->send(pkt);
|
|
|
|
|
LOG_INFO("Sent CMSG_CANCEL_AURA (mount spell ", savedMountAura, ") — Classic fallback");
|
|
|
|
|
} else {
|
2026-03-28 11:35:10 +03:00
|
|
|
for (const auto& a : owner_.getPlayerAuras()) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == owner_.playerGuid) {
|
|
|
|
|
auto pkt = CancelAuraPacket::build(a.spellId);
|
|
|
|
|
owner_.socket->send(pkt);
|
|
|
|
|
LOG_INFO("Sent CMSG_CANCEL_AURA (spell ", a.spellId, ") — brute force dismount");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Force Speed / Root / Flag Change Handlers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-03-29 19:08:42 -07:00
|
|
|
// Shared force-ACK packet builder. All server-forced movement changes (speed,
|
|
|
|
|
// root, flag, collision-height, knockback) require the same ACK structure:
|
|
|
|
|
// GUID + counter + movement payload with server-space coordinates. Centralised
|
|
|
|
|
// here so transport coordinate conversion can't diverge between handlers.
|
|
|
|
|
network::Packet MovementHandler::buildForceAck(Opcode ackOpcode, uint32_t counter) {
|
|
|
|
|
network::Packet ack(wireOpcode(ackOpcode));
|
|
|
|
|
const bool legacyGuid =
|
|
|
|
|
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
|
|
|
|
|
if (legacyGuid) {
|
|
|
|
|
ack.writeUInt64(owner_.playerGuid);
|
|
|
|
|
} else {
|
|
|
|
|
ack.writePackedGuid(owner_.playerGuid);
|
|
|
|
|
}
|
|
|
|
|
ack.writeUInt32(counter);
|
|
|
|
|
|
|
|
|
|
MovementInfo wire = movementInfo;
|
|
|
|
|
wire.time = nextMovementTimestampMs();
|
|
|
|
|
if (wire.hasFlag(MovementFlags::ONTRANSPORT)) {
|
|
|
|
|
wire.transportTime = wire.time;
|
|
|
|
|
wire.transportTime2 = wire.time;
|
|
|
|
|
}
|
|
|
|
|
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z));
|
|
|
|
|
wire.x = serverPos.x;
|
|
|
|
|
wire.y = serverPos.y;
|
|
|
|
|
wire.z = serverPos.z;
|
|
|
|
|
if (wire.hasFlag(MovementFlags::ONTRANSPORT)) {
|
|
|
|
|
glm::vec3 serverTransport =
|
|
|
|
|
core::coords::canonicalToServer(glm::vec3(wire.transportX, wire.transportY, wire.transportZ));
|
|
|
|
|
wire.transportX = serverTransport.x;
|
|
|
|
|
wire.transportY = serverTransport.y;
|
|
|
|
|
wire.transportZ = serverTransport.z;
|
|
|
|
|
}
|
|
|
|
|
if (owner_.packetParsers_) {
|
|
|
|
|
owner_.packetParsers_->writeMovementPayload(ack, wire);
|
|
|
|
|
} else {
|
|
|
|
|
MovementPacket::writeMovementPayload(ack, wire);
|
|
|
|
|
}
|
|
|
|
|
return ack;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
void MovementHandler::handleForceSpeedChange(network::Packet& packet, const char* name,
|
|
|
|
|
Opcode ackOpcode, float* speedStorage) {
|
|
|
|
|
const bool fscTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
|
|
|
|
uint64_t guid = fscTbcLike
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
|
|
|
|
uint32_t counter = packet.readUInt32();
|
|
|
|
|
|
2026-03-29 20:53:26 -07:00
|
|
|
size_t remaining = packet.getRemainingSize();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (remaining >= 8) {
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
} else if (remaining >= 5) {
|
|
|
|
|
packet.readUInt8();
|
|
|
|
|
}
|
|
|
|
|
float newSpeed = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_FORCE_", name, "_CHANGE: guid=0x", std::hex, guid, std::dec,
|
|
|
|
|
" counter=", counter, " speed=", newSpeed);
|
|
|
|
|
|
|
|
|
|
if (guid != owner_.playerGuid) return;
|
|
|
|
|
|
2026-03-29 18:53:16 -07:00
|
|
|
// Validate BEFORE sending ACK — if we echo a bad speed back to the server
|
|
|
|
|
// but don't apply it locally, the client and server desync on movement speed.
|
|
|
|
|
if (std::isnan(newSpeed) || newSpeed < 0.1f || newSpeed > 100.0f) {
|
|
|
|
|
LOG_WARNING("Ignoring invalid ", name, " speed: ", newSpeed);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (owner_.socket) {
|
2026-03-29 19:08:42 -07:00
|
|
|
auto ack = buildForceAck(ackOpcode, counter);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
ack.writeFloat(newSpeed);
|
|
|
|
|
owner_.socket->send(ack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (speedStorage) *speedStorage = newSpeed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleForceRunSpeedChange(network::Packet& packet) {
|
|
|
|
|
handleForceSpeedChange(packet, "RUN_SPEED", Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, &serverRunSpeed_);
|
|
|
|
|
|
|
|
|
|
if (!onTaxiFlight_ && !taxiMountActive_ && owner_.currentMountDisplayId_ != 0 && serverRunSpeed_ <= 8.5f) {
|
|
|
|
|
LOG_INFO("Auto-clearing mount from speed change: speed=", serverRunSpeed_,
|
|
|
|
|
" displayId=", owner_.currentMountDisplayId_);
|
|
|
|
|
owner_.currentMountDisplayId_ = 0;
|
|
|
|
|
if (owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) {
|
|
|
|
|
const bool rootTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < (rootTbc ? 8u : 2u)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint64_t guid = rootTbc
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(4)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint32_t counter = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
LOG_INFO(rooted ? "SMSG_FORCE_MOVE_ROOT" : "SMSG_FORCE_MOVE_UNROOT",
|
|
|
|
|
": guid=0x", std::hex, guid, std::dec, " counter=", counter);
|
|
|
|
|
|
|
|
|
|
if (guid != owner_.playerGuid) return;
|
|
|
|
|
|
|
|
|
|
if (rooted) {
|
|
|
|
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ROOT);
|
|
|
|
|
} else {
|
|
|
|
|
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ROOT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!owner_.socket) return;
|
2026-03-29 19:08:42 -07:00
|
|
|
Opcode ackOp = rooted ? Opcode::CMSG_FORCE_MOVE_ROOT_ACK : Opcode::CMSG_FORCE_MOVE_UNROOT_ACK;
|
|
|
|
|
if (wireOpcode(ackOp) == 0xFFFF) return;
|
|
|
|
|
owner_.socket->send(buildForceAck(ackOp, counter));
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleForceMoveFlagChange(network::Packet& packet, const char* name,
|
|
|
|
|
Opcode ackOpcode, uint32_t flag, bool set) {
|
|
|
|
|
const bool fmfTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < (fmfTbcLike ? 8u : 2u)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint64_t guid = fmfTbcLike
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(4)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint32_t counter = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_FORCE_", name, ": guid=0x", std::hex, guid, std::dec, " counter=", counter);
|
|
|
|
|
|
|
|
|
|
if (guid != owner_.playerGuid) return;
|
|
|
|
|
|
|
|
|
|
if (flag != 0) {
|
|
|
|
|
if (set) {
|
|
|
|
|
movementInfo.flags |= flag;
|
|
|
|
|
} else {
|
|
|
|
|
movementInfo.flags &= ~flag;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!owner_.socket) return;
|
2026-03-29 19:08:42 -07:00
|
|
|
if (wireOpcode(ackOpcode) == 0xFFFF) return;
|
|
|
|
|
owner_.socket->send(buildForceAck(ackOpcode, counter));
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleMoveSetCollisionHeight(network::Packet& packet) {
|
|
|
|
|
const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < (legacyGuid ? 8u : 2u)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint64_t guid = legacyGuid ? packet.readUInt64() : packet.readPackedGuid();
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(8)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint32_t counter = packet.readUInt32();
|
|
|
|
|
float height = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_MOVE_SET_COLLISION_HGT: guid=0x", std::hex, guid, std::dec,
|
|
|
|
|
" counter=", counter, " height=", height);
|
|
|
|
|
|
|
|
|
|
if (guid != owner_.playerGuid) return;
|
|
|
|
|
if (!owner_.socket) return;
|
|
|
|
|
|
2026-03-29 19:08:42 -07:00
|
|
|
if (wireOpcode(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK) == 0xFFFF) return;
|
|
|
|
|
// buildForceAck now handles transport coordinate conversion, fixing the
|
|
|
|
|
// previous omission that caused desync when riding boats/zeppelins.
|
|
|
|
|
auto ack = buildForceAck(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK, counter);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
ack.writeFloat(height);
|
|
|
|
|
owner_.socket->send(ack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleMoveKnockBack(network::Packet& packet) {
|
|
|
|
|
const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < (mkbTbc ? 8u : 2u)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint64_t guid = mkbTbc
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(20)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint32_t counter = packet.readUInt32();
|
|
|
|
|
float vcos = packet.readFloat();
|
|
|
|
|
float vsin = packet.readFloat();
|
|
|
|
|
float hspeed = packet.readFloat();
|
|
|
|
|
float vspeed = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_MOVE_KNOCK_BACK: guid=0x", std::hex, guid, std::dec,
|
|
|
|
|
" counter=", counter, " vcos=", vcos, " vsin=", vsin,
|
|
|
|
|
" hspeed=", hspeed, " vspeed=", vspeed);
|
|
|
|
|
|
|
|
|
|
if (guid != owner_.playerGuid) return;
|
|
|
|
|
|
|
|
|
|
if (owner_.knockBackCallback_) {
|
|
|
|
|
owner_.knockBackCallback_(vcos, vsin, hspeed, vspeed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!owner_.socket) return;
|
2026-03-29 19:08:42 -07:00
|
|
|
if (wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK) == 0xFFFF) return;
|
|
|
|
|
owner_.socket->send(buildForceAck(Opcode::CMSG_MOVE_KNOCK_BACK_ACK, counter));
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Other Player / Creature Movement Handlers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleMoveSetSpeed(network::Packet& packet) {
|
|
|
|
|
const bool useFull = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
|
|
|
|
uint64_t moverGuid = useFull
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
|
|
|
|
|
2026-03-29 20:53:26 -07:00
|
|
|
const size_t remaining = packet.getRemainingSize();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (remaining < 4) return;
|
|
|
|
|
if (remaining > 4) {
|
|
|
|
|
packet.setReadPos(packet.getSize() - 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float speed = packet.readFloat();
|
|
|
|
|
if (!std::isfinite(speed) || speed <= 0.01f || speed > 200.0f) return;
|
|
|
|
|
|
|
|
|
|
if (moverGuid != owner_.playerGuid) return;
|
|
|
|
|
const uint16_t wireOp = packet.getOpcode();
|
|
|
|
|
if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_RUN_SPEED)) serverRunSpeed_ = speed;
|
|
|
|
|
else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_RUN_BACK_SPEED)) serverRunBackSpeed_ = speed;
|
|
|
|
|
else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_WALK_SPEED)) serverWalkSpeed_ = speed;
|
|
|
|
|
else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_SWIM_SPEED)) serverSwimSpeed_ = speed;
|
|
|
|
|
else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED)) serverSwimBackSpeed_ = speed;
|
|
|
|
|
else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_FLIGHT_SPEED)) serverFlightSpeed_ = speed;
|
|
|
|
|
else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED))serverFlightBackSpeed_= speed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleOtherPlayerMovement(network::Packet& packet) {
|
|
|
|
|
const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
|
|
|
|
uint64_t moverGuid = otherMoveTbc
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
|
|
|
|
if (moverGuid == owner_.playerGuid || moverGuid == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MovementInfo info = {};
|
|
|
|
|
info.flags = packet.readUInt32();
|
|
|
|
|
uint8_t flags2Size = owner_.packetParsers_ ? owner_.packetParsers_->movementFlags2Size() : 2;
|
|
|
|
|
if (flags2Size == 2) info.flags2 = packet.readUInt16();
|
|
|
|
|
else if (flags2Size == 1) info.flags2 = packet.readUInt8();
|
|
|
|
|
info.time = packet.readUInt32();
|
|
|
|
|
info.x = packet.readFloat();
|
|
|
|
|
info.y = packet.readFloat();
|
|
|
|
|
info.z = packet.readFloat();
|
|
|
|
|
info.orientation = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
const uint32_t wireTransportFlag = owner_.packetParsers_ ? owner_.packetParsers_->wireOnTransportFlag() : 0x00000200;
|
|
|
|
|
const bool onTransport = (info.flags & wireTransportFlag) != 0;
|
|
|
|
|
uint64_t transportGuid = 0;
|
|
|
|
|
float tLocalX = 0, tLocalY = 0, tLocalZ = 0, tLocalO = 0;
|
|
|
|
|
if (onTransport) {
|
|
|
|
|
transportGuid = packet.readPackedGuid();
|
|
|
|
|
tLocalX = packet.readFloat();
|
|
|
|
|
tLocalY = packet.readFloat();
|
|
|
|
|
tLocalZ = packet.readFloat();
|
|
|
|
|
tLocalO = packet.readFloat();
|
|
|
|
|
if (flags2Size >= 1) {
|
|
|
|
|
/*uint32_t transportTime =*/ packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
if (flags2Size >= 2) {
|
|
|
|
|
/*int8_t transportSeat =*/ packet.readUInt8();
|
|
|
|
|
if (info.flags2 & 0x0200) {
|
|
|
|
|
/*uint32_t transportTime2 =*/ packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto entity = owner_.getEntityManager().getEntity(moverGuid);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!entity) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(info.x, info.y, info.z));
|
|
|
|
|
float canYaw = core::coords::serverToCanonicalYaw(info.orientation);
|
|
|
|
|
|
|
|
|
|
if (onTransport && transportGuid != 0 && owner_.transportManager_) {
|
|
|
|
|
glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(tLocalX, tLocalY, tLocalZ));
|
|
|
|
|
owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, true,
|
|
|
|
|
core::coords::serverToCanonicalYaw(tLocalO));
|
|
|
|
|
glm::vec3 worldPos = owner_.transportManager_->getPlayerWorldPosition(transportGuid, localCanonical);
|
|
|
|
|
canonical = worldPos;
|
|
|
|
|
} else if (!onTransport) {
|
|
|
|
|
owner_.clearTransportAttachment(moverGuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t durationMs = 120;
|
|
|
|
|
auto itPrev = otherPlayerMoveTimeMs_.find(moverGuid);
|
|
|
|
|
if (itPrev != otherPlayerMoveTimeMs_.end()) {
|
|
|
|
|
uint32_t rawDt = info.time - itPrev->second;
|
|
|
|
|
if (rawDt >= 20 && rawDt <= 2000) {
|
|
|
|
|
float fDt = static_cast<float>(rawDt);
|
|
|
|
|
auto& smoothed = otherPlayerSmoothedIntervalMs_[moverGuid];
|
|
|
|
|
if (smoothed < 1.0f) smoothed = fDt;
|
|
|
|
|
smoothed = 0.7f * smoothed + 0.3f * fDt;
|
|
|
|
|
float clamped = std::max(60.0f, std::min(500.0f, smoothed));
|
|
|
|
|
durationMs = static_cast<uint32_t>(clamped);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
otherPlayerMoveTimeMs_[moverGuid] = info.time;
|
|
|
|
|
|
|
|
|
|
const uint16_t wireOp = packet.getOpcode();
|
|
|
|
|
const bool isStopOpcode =
|
|
|
|
|
(wireOp == wireOpcode(Opcode::MSG_MOVE_STOP)) ||
|
|
|
|
|
(wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_STRAFE)) ||
|
|
|
|
|
(wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_TURN)) ||
|
|
|
|
|
(wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_SWIM)) ||
|
|
|
|
|
(wireOp == wireOpcode(Opcode::MSG_MOVE_FALL_LAND));
|
|
|
|
|
const bool isJumpOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_JUMP));
|
|
|
|
|
|
|
|
|
|
const float entityDuration = isStopOpcode ? 0.0f : (durationMs / 1000.0f);
|
|
|
|
|
entity->startMoveTo(canonical.x, canonical.y, canonical.z, canYaw, entityDuration);
|
|
|
|
|
|
|
|
|
|
if (owner_.creatureMoveCallback_) {
|
|
|
|
|
const uint32_t notifyDuration = isStopOpcode ? 0u : durationMs;
|
|
|
|
|
owner_.creatureMoveCallback_(moverGuid, canonical.x, canonical.y, canonical.z, notifyDuration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.unitAnimHintCallback_ && isJumpOpcode) {
|
|
|
|
|
owner_.unitAnimHintCallback_(moverGuid, 38u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.unitMoveFlagsCallback_) {
|
|
|
|
|
owner_.unitMoveFlagsCallback_(moverGuid, info.flags);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleCompressedMoves(network::Packet& packet) {
|
|
|
|
|
std::vector<uint8_t> decompressedStorage;
|
|
|
|
|
const std::vector<uint8_t>* dataPtr = &packet.getData();
|
|
|
|
|
|
|
|
|
|
const auto& rawData = packet.getData();
|
|
|
|
|
const bool hasCompressedWrapper =
|
|
|
|
|
rawData.size() >= 6 &&
|
|
|
|
|
rawData[4] == 0x78 &&
|
|
|
|
|
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
|
|
|
|
|
rawData[5] == 0xDA || rawData[5] == 0x5E);
|
|
|
|
|
if (hasCompressedWrapper) {
|
|
|
|
|
uint32_t decompressedSize = static_cast<uint32_t>(rawData[0]) |
|
|
|
|
|
(static_cast<uint32_t>(rawData[1]) << 8) |
|
|
|
|
|
(static_cast<uint32_t>(rawData[2]) << 16) |
|
|
|
|
|
(static_cast<uint32_t>(rawData[3]) << 24);
|
|
|
|
|
if (decompressedSize == 0 || decompressedSize > 65536) {
|
|
|
|
|
LOG_WARNING("SMSG_COMPRESSED_MOVES: bad decompressedSize=", decompressedSize);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decompressedStorage.resize(decompressedSize);
|
|
|
|
|
uLongf destLen = decompressedSize;
|
|
|
|
|
int ret = uncompress(decompressedStorage.data(), &destLen,
|
|
|
|
|
rawData.data() + 4, rawData.size() - 4);
|
|
|
|
|
if (ret != Z_OK) {
|
|
|
|
|
LOG_WARNING("SMSG_COMPRESSED_MOVES: zlib error ", ret);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decompressedStorage.resize(destLen);
|
|
|
|
|
dataPtr = &decompressedStorage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto& data = *dataPtr;
|
|
|
|
|
const size_t dataLen = data.size();
|
|
|
|
|
|
|
|
|
|
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
|
|
|
|
|
uint16_t monsterMoveTransportWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE_TRANSPORT);
|
|
|
|
|
|
|
|
|
|
const std::array<uint16_t, 29> kMoveOpcodes = {
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_FORWARD),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_BACKWARD),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_STOP),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_STRAFE_LEFT),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_STRAFE_RIGHT),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_STOP_STRAFE),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_JUMP),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_TURN_LEFT),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_TURN_RIGHT),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_STOP_TURN),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_SET_FACING),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_FALL_LAND),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_HEARTBEAT),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_SWIM),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_STOP_SWIM),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_SET_WALK_MODE),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_SET_RUN_MODE),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_PITCH_UP),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_PITCH_DOWN),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_STOP_PITCH),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_ASCEND),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_STOP_ASCEND),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_START_DESCEND),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_SET_PITCH),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_GRAVITY_CHNG),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_UPDATE_CAN_FLY),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_ROOT),
|
|
|
|
|
wireOpcode(Opcode::MSG_MOVE_UNROOT),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct CompressedMoveSubPacket {
|
|
|
|
|
uint16_t opcode = 0;
|
|
|
|
|
std::vector<uint8_t> payload;
|
|
|
|
|
};
|
|
|
|
|
struct DecodeResult {
|
|
|
|
|
bool ok = false;
|
|
|
|
|
bool overrun = false;
|
|
|
|
|
bool usedPayloadOnlySize = false;
|
|
|
|
|
size_t endPos = 0;
|
|
|
|
|
size_t recognizedCount = 0;
|
|
|
|
|
size_t subPacketCount = 0;
|
|
|
|
|
std::vector<CompressedMoveSubPacket> packets;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto isRecognizedSubOpcode = [&](uint16_t subOpcode) {
|
|
|
|
|
return subOpcode == monsterMoveWire ||
|
|
|
|
|
subOpcode == monsterMoveTransportWire ||
|
|
|
|
|
std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), subOpcode) != kMoveOpcodes.end();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto decodeSubPackets = [&](bool payloadOnlySize) -> DecodeResult {
|
|
|
|
|
DecodeResult result;
|
|
|
|
|
result.usedPayloadOnlySize = payloadOnlySize;
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while (pos < dataLen) {
|
|
|
|
|
if (pos + 1 > dataLen) break;
|
|
|
|
|
uint8_t subSize = data[pos];
|
|
|
|
|
if (subSize == 0) {
|
|
|
|
|
result.ok = true;
|
|
|
|
|
result.endPos = pos + 1;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t payloadLen = payloadOnlySize
|
|
|
|
|
? static_cast<size_t>(subSize)
|
|
|
|
|
: (subSize >= 2 ? static_cast<size_t>(subSize) - 2 : 0);
|
|
|
|
|
if (!payloadOnlySize && subSize < 2) {
|
|
|
|
|
result.endPos = pos;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t packetLen = 1 + 2 + payloadLen;
|
|
|
|
|
if (pos + packetLen > dataLen) {
|
|
|
|
|
result.overrun = true;
|
|
|
|
|
result.endPos = pos;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint16_t subOpcode = static_cast<uint16_t>(data[pos + 1]) |
|
|
|
|
|
(static_cast<uint16_t>(data[pos + 2]) << 8);
|
|
|
|
|
size_t payloadStart = pos + 3;
|
|
|
|
|
|
|
|
|
|
CompressedMoveSubPacket subPacket;
|
|
|
|
|
subPacket.opcode = subOpcode;
|
|
|
|
|
subPacket.payload.assign(data.begin() + payloadStart,
|
|
|
|
|
data.begin() + payloadStart + payloadLen);
|
|
|
|
|
result.packets.push_back(std::move(subPacket));
|
|
|
|
|
++result.subPacketCount;
|
|
|
|
|
if (isRecognizedSubOpcode(subOpcode)) {
|
|
|
|
|
++result.recognizedCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pos += packetLen;
|
|
|
|
|
}
|
|
|
|
|
result.ok = (result.endPos == 0 || result.endPos == dataLen);
|
|
|
|
|
result.endPos = dataLen;
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DecodeResult decoded = decodeSubPackets(false);
|
|
|
|
|
if (!decoded.ok || decoded.overrun) {
|
|
|
|
|
DecodeResult payloadOnlyDecoded = decodeSubPackets(true);
|
|
|
|
|
const bool preferPayloadOnly =
|
|
|
|
|
payloadOnlyDecoded.ok &&
|
|
|
|
|
(!decoded.ok || decoded.overrun || payloadOnlyDecoded.recognizedCount > decoded.recognizedCount);
|
|
|
|
|
if (preferPayloadOnly) {
|
|
|
|
|
decoded = std::move(payloadOnlyDecoded);
|
|
|
|
|
static uint32_t payloadOnlyFallbackCount = 0;
|
|
|
|
|
++payloadOnlyFallbackCount;
|
|
|
|
|
if (payloadOnlyFallbackCount <= 10 || (payloadOnlyFallbackCount % 100) == 0) {
|
|
|
|
|
LOG_WARNING("SMSG_COMPRESSED_MOVES decoded via payload-only size fallback",
|
|
|
|
|
" (occurrence=", payloadOnlyFallbackCount, ")");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!decoded.ok || decoded.overrun) {
|
|
|
|
|
LOG_WARNING("SMSG_COMPRESSED_MOVES: sub-packet overruns buffer at pos=", decoded.endPos);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_set<uint16_t> unhandledSeen;
|
|
|
|
|
|
|
|
|
|
for (const auto& entry : decoded.packets) {
|
|
|
|
|
network::Packet subPacket(entry.opcode, entry.payload);
|
|
|
|
|
|
|
|
|
|
if (entry.opcode == monsterMoveWire) {
|
|
|
|
|
handleMonsterMove(subPacket);
|
|
|
|
|
} else if (entry.opcode == monsterMoveTransportWire) {
|
|
|
|
|
handleMonsterMoveTransport(subPacket);
|
|
|
|
|
} else if (owner_.state == WorldState::IN_WORLD &&
|
|
|
|
|
std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), entry.opcode) != kMoveOpcodes.end()) {
|
|
|
|
|
handleOtherPlayerMovement(subPacket);
|
|
|
|
|
} else {
|
|
|
|
|
if (unhandledSeen.insert(entry.opcode).second) {
|
|
|
|
|
LOG_INFO("SMSG_COMPRESSED_MOVES: unhandled sub-opcode 0x",
|
|
|
|
|
std::hex, entry.opcode, std::dec, " payloadLen=", entry.payload.size());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Monster Move Handlers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleMonsterMove(network::Packet& packet) {
|
|
|
|
|
if (isActiveExpansion("classic") || isActiveExpansion("turtle")) {
|
|
|
|
|
constexpr uint32_t kMaxMonsterMovesPerTick = 256;
|
|
|
|
|
++monsterMovePacketsThisTick_;
|
|
|
|
|
if (monsterMovePacketsThisTick_ > kMaxMonsterMovesPerTick) {
|
|
|
|
|
++monsterMovePacketsDroppedThisTick_;
|
|
|
|
|
if (monsterMovePacketsDroppedThisTick_ <= 3 ||
|
|
|
|
|
(monsterMovePacketsDroppedThisTick_ % 100) == 0) {
|
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE: per-tick cap exceeded, dropping packet",
|
|
|
|
|
" (processed=", monsterMovePacketsThisTick_,
|
|
|
|
|
" dropped=", monsterMovePacketsDroppedThisTick_, ")");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MonsterMoveData data;
|
|
|
|
|
auto logMonsterMoveParseFailure = [&](const std::string& msg) {
|
|
|
|
|
static uint32_t failCount = 0;
|
|
|
|
|
++failCount;
|
|
|
|
|
if (failCount <= 10 || (failCount % 100) == 0) {
|
|
|
|
|
LOG_WARNING(msg, " (occurrence=", failCount, ")");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
auto logWrappedUncompressedFallbackUsed = [&]() {
|
|
|
|
|
static uint32_t wrappedUncompressedFallbackCount = 0;
|
|
|
|
|
++wrappedUncompressedFallbackCount;
|
|
|
|
|
if (wrappedUncompressedFallbackCount <= 10 || (wrappedUncompressedFallbackCount % 100) == 0) {
|
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback",
|
|
|
|
|
" (occurrence=", wrappedUncompressedFallbackCount, ")");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
auto stripWrappedSubpacket = [&](const std::vector<uint8_t>& bytes, std::vector<uint8_t>& stripped) -> bool {
|
|
|
|
|
if (bytes.size() < 3) return false;
|
|
|
|
|
uint8_t subSize = bytes[0];
|
|
|
|
|
if (subSize < 2) return false;
|
|
|
|
|
size_t wrappedLen = static_cast<size_t>(subSize) + 1;
|
|
|
|
|
if (wrappedLen != bytes.size()) return false;
|
|
|
|
|
size_t payloadLen = static_cast<size_t>(subSize) - 2;
|
|
|
|
|
if (3 + payloadLen > bytes.size()) return false;
|
|
|
|
|
stripped.assign(bytes.begin() + 3, bytes.begin() + 3 + payloadLen);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const auto& rawData = packet.getData();
|
|
|
|
|
const bool allowTurtleMoveCompression = isActiveExpansion("turtle");
|
|
|
|
|
bool isCompressed = allowTurtleMoveCompression &&
|
|
|
|
|
rawData.size() >= 6 &&
|
|
|
|
|
rawData[4] == 0x78 &&
|
|
|
|
|
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
|
|
|
|
|
rawData[5] == 0xDA || rawData[5] == 0x5E);
|
|
|
|
|
if (isCompressed) {
|
|
|
|
|
uint32_t decompSize = static_cast<uint32_t>(rawData[0]) |
|
|
|
|
|
(static_cast<uint32_t>(rawData[1]) << 8) |
|
|
|
|
|
(static_cast<uint32_t>(rawData[2]) << 16) |
|
|
|
|
|
(static_cast<uint32_t>(rawData[3]) << 24);
|
|
|
|
|
if (decompSize == 0 || decompSize > 65536) {
|
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE: bad decompSize=", decompSize);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
std::vector<uint8_t> decompressed(decompSize);
|
|
|
|
|
uLongf destLen = decompSize;
|
|
|
|
|
int ret = uncompress(decompressed.data(), &destLen,
|
|
|
|
|
rawData.data() + 4, rawData.size() - 4);
|
|
|
|
|
if (ret != Z_OK) {
|
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE: zlib error ", ret);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
decompressed.resize(destLen);
|
|
|
|
|
std::vector<uint8_t> stripped;
|
|
|
|
|
bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped);
|
|
|
|
|
|
|
|
|
|
bool parsed = false;
|
|
|
|
|
if (hasWrappedForm) {
|
|
|
|
|
network::Packet wrappedPacket(packet.getOpcode(), stripped);
|
|
|
|
|
if (owner_.packetParsers_->parseMonsterMove(wrappedPacket, data)) {
|
|
|
|
|
parsed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
network::Packet decompPacket(packet.getOpcode(), decompressed);
|
|
|
|
|
if (owner_.packetParsers_->parseMonsterMove(decompPacket, data)) {
|
|
|
|
|
parsed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
if (hasWrappedForm) {
|
|
|
|
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
|
|
|
|
|
std::to_string(destLen) + " bytes, wrapped payload " +
|
|
|
|
|
std::to_string(stripped.size()) + " bytes)");
|
|
|
|
|
} else {
|
|
|
|
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
|
|
|
|
|
std::to_string(destLen) + " bytes)");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else if (!owner_.packetParsers_->parseMonsterMove(packet, data)) {
|
|
|
|
|
std::vector<uint8_t> stripped;
|
|
|
|
|
if (stripWrappedSubpacket(rawData, stripped)) {
|
|
|
|
|
network::Packet wrappedPacket(packet.getOpcode(), stripped);
|
|
|
|
|
if (owner_.packetParsers_->parseMonsterMove(wrappedPacket, data)) {
|
|
|
|
|
logWrappedUncompressedFallbackUsed();
|
|
|
|
|
} else {
|
|
|
|
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto entity = owner_.getEntityManager().getEntity(data.guid);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!entity) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.hasDest) {
|
|
|
|
|
glm::vec3 destCanonical = core::coords::serverToCanonical(
|
|
|
|
|
glm::vec3(data.destX, data.destY, data.destZ));
|
|
|
|
|
|
|
|
|
|
float orientation = entity->getOrientation();
|
|
|
|
|
if (data.moveType == 4) {
|
|
|
|
|
orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
|
|
|
|
|
} else if (data.moveType == 3) {
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto target = owner_.getEntityManager().getEntity(data.facingTarget);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (target) {
|
|
|
|
|
float dx = target->getX() - entity->getX();
|
|
|
|
|
float dy = target->getY() - entity->getY();
|
|
|
|
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
|
|
|
|
orientation = std::atan2(-dy, dx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
float dx = destCanonical.x - entity->getX();
|
|
|
|
|
float dy = destCanonical.y - entity->getY();
|
|
|
|
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
|
|
|
|
orientation = std::atan2(-dy, dx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.moveType != 3) {
|
|
|
|
|
glm::vec3 startCanonical = core::coords::serverToCanonical(
|
|
|
|
|
glm::vec3(data.x, data.y, data.z));
|
|
|
|
|
float travelDx = destCanonical.x - startCanonical.x;
|
|
|
|
|
float travelDy = destCanonical.y - startCanonical.y;
|
|
|
|
|
float travelLen = std::sqrt(travelDx * travelDx + travelDy * travelDy);
|
|
|
|
|
if (travelLen > 0.5f) {
|
|
|
|
|
float travelAngle = std::atan2(-travelDy, travelDx);
|
|
|
|
|
float diff = orientation - travelAngle;
|
|
|
|
|
while (diff > static_cast<float>(M_PI)) diff -= 2.0f * static_cast<float>(M_PI);
|
|
|
|
|
while (diff < -static_cast<float>(M_PI)) diff += 2.0f * static_cast<float>(M_PI);
|
|
|
|
|
if (std::abs(diff) > static_cast<float>(M_PI) * 0.5f) {
|
|
|
|
|
orientation = travelAngle;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
|
|
|
|
orientation, data.duration / 1000.0f);
|
|
|
|
|
|
|
|
|
|
if (owner_.creatureMoveCallback_) {
|
|
|
|
|
owner_.creatureMoveCallback_(data.guid,
|
|
|
|
|
destCanonical.x, destCanonical.y, destCanonical.z,
|
|
|
|
|
data.duration);
|
|
|
|
|
}
|
|
|
|
|
} else if (data.moveType == 1) {
|
|
|
|
|
glm::vec3 posCanonical = core::coords::serverToCanonical(
|
|
|
|
|
glm::vec3(data.x, data.y, data.z));
|
|
|
|
|
entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z,
|
|
|
|
|
entity->getOrientation());
|
|
|
|
|
|
|
|
|
|
if (owner_.creatureMoveCallback_) {
|
|
|
|
|
owner_.creatureMoveCallback_(data.guid,
|
|
|
|
|
posCanonical.x, posCanonical.y, posCanonical.z, 0);
|
|
|
|
|
}
|
|
|
|
|
} else if (data.moveType == 4) {
|
|
|
|
|
float orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
|
|
|
|
|
glm::vec3 posCanonical = core::coords::serverToCanonical(
|
|
|
|
|
glm::vec3(data.x, data.y, data.z));
|
|
|
|
|
entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z, orientation);
|
|
|
|
|
if (owner_.creatureMoveCallback_) {
|
|
|
|
|
owner_.creatureMoveCallback_(data.guid,
|
|
|
|
|
posCanonical.x, posCanonical.y, posCanonical.z, 0);
|
|
|
|
|
}
|
|
|
|
|
} else if (data.moveType == 3 && data.facingTarget != 0) {
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto target = owner_.getEntityManager().getEntity(data.facingTarget);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (target) {
|
|
|
|
|
float dx = target->getX() - entity->getX();
|
|
|
|
|
float dy = target->getY() - entity->getY();
|
|
|
|
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
|
|
|
|
float orientation = std::atan2(-dy, dx);
|
|
|
|
|
entity->setOrientation(orientation);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleMonsterMoveTransport(network::Packet& packet) {
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < 8 + 1 + 8 + 12) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint64_t moverGuid = packet.readUInt64();
|
|
|
|
|
/*uint8_t unk =*/ packet.readUInt8();
|
|
|
|
|
uint64_t transportGuid = packet.readUInt64();
|
|
|
|
|
|
|
|
|
|
float localX = packet.readFloat();
|
|
|
|
|
float localY = packet.readFloat();
|
|
|
|
|
float localZ = packet.readFloat();
|
|
|
|
|
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto entity = owner_.getEntityManager().getEntity(moverGuid);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!entity) return;
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 5 > packet.getSize()) {
|
|
|
|
|
if (owner_.transportManager_) {
|
|
|
|
|
glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
|
|
|
|
|
owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, false, 0.0f);
|
|
|
|
|
glm::vec3 worldPos = owner_.transportManager_->getPlayerWorldPosition(transportGuid, localCanonical);
|
|
|
|
|
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
|
|
|
|
|
if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_)
|
|
|
|
|
owner_.creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*uint32_t splineId =*/ packet.readUInt32();
|
|
|
|
|
uint8_t moveType = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
if (moveType == 1) {
|
|
|
|
|
if (owner_.transportManager_) {
|
|
|
|
|
glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
|
|
|
|
|
owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, false, 0.0f);
|
|
|
|
|
glm::vec3 worldPos = owner_.transportManager_->getPlayerWorldPosition(transportGuid, localCanonical);
|
|
|
|
|
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
|
|
|
|
|
if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_)
|
|
|
|
|
owner_.creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float facingAngle = entity->getOrientation();
|
|
|
|
|
if (moveType == 2) {
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return;
|
|
|
|
|
float sx = packet.readFloat(), sy = packet.readFloat(), sz = packet.readFloat();
|
|
|
|
|
facingAngle = std::atan2(-(sy - localY), sx - localX);
|
|
|
|
|
(void)sz;
|
|
|
|
|
} else if (moveType == 3) {
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return;
|
|
|
|
|
uint64_t tgtGuid = packet.readUInt64();
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
if (auto tgt = owner_.getEntityManager().getEntity(tgtGuid)) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
float dx = tgt->getX() - entity->getX();
|
|
|
|
|
float dy = tgt->getY() - entity->getY();
|
|
|
|
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f)
|
|
|
|
|
facingAngle = std::atan2(-dy, dx);
|
|
|
|
|
}
|
|
|
|
|
} else if (moveType == 4) {
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
|
|
|
|
facingAngle = core::coords::serverToCanonicalYaw(packet.readFloat());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
|
|
|
|
uint32_t splineFlags = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
if (splineFlags & 0x00400000) {
|
|
|
|
|
if (packet.getReadPos() + 5 > packet.getSize()) return;
|
|
|
|
|
packet.readUInt8(); packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
|
|
|
|
uint32_t duration = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
if (splineFlags & 0x00000800) {
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return;
|
|
|
|
|
packet.readFloat(); packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
|
|
|
|
uint32_t pointCount = packet.readUInt32();
|
|
|
|
|
constexpr uint32_t kMaxTransportSplinePoints = 1000;
|
|
|
|
|
if (pointCount > kMaxTransportSplinePoints) {
|
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: pointCount=", pointCount,
|
|
|
|
|
" clamped to ", kMaxTransportSplinePoints);
|
|
|
|
|
pointCount = kMaxTransportSplinePoints;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float destLocalX = localX, destLocalY = localY, destLocalZ = localZ;
|
|
|
|
|
bool hasDest = false;
|
|
|
|
|
if (pointCount > 0) {
|
|
|
|
|
const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0;
|
|
|
|
|
if (uncompressed) {
|
|
|
|
|
for (uint32_t i = 0; i < pointCount - 1; ++i) {
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
|
|
|
|
}
|
|
|
|
|
if (packet.getReadPos() + 12 <= packet.getSize()) {
|
|
|
|
|
destLocalX = packet.readFloat();
|
|
|
|
|
destLocalY = packet.readFloat();
|
|
|
|
|
destLocalZ = packet.readFloat();
|
|
|
|
|
hasDest = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (packet.getReadPos() + 12 <= packet.getSize()) {
|
|
|
|
|
destLocalX = packet.readFloat();
|
|
|
|
|
destLocalY = packet.readFloat();
|
|
|
|
|
destLocalZ = packet.readFloat();
|
|
|
|
|
hasDest = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!owner_.transportManager_) {
|
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: TransportManager not available for mover 0x",
|
|
|
|
|
std::hex, moverGuid, std::dec);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 startLocalCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
|
|
|
|
|
|
|
|
|
|
if (hasDest && duration > 0) {
|
|
|
|
|
glm::vec3 destLocalCanonical = core::coords::serverToCanonical(glm::vec3(destLocalX, destLocalY, destLocalZ));
|
|
|
|
|
glm::vec3 destWorld = owner_.transportManager_->getPlayerWorldPosition(transportGuid, destLocalCanonical);
|
|
|
|
|
|
|
|
|
|
if (moveType == 0) {
|
|
|
|
|
float dx = destLocalCanonical.x - startLocalCanonical.x;
|
|
|
|
|
float dy = destLocalCanonical.y - startLocalCanonical.y;
|
|
|
|
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f)
|
|
|
|
|
facingAngle = std::atan2(-dy, dx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, destLocalCanonical, false, 0.0f);
|
|
|
|
|
entity->startMoveTo(destWorld.x, destWorld.y, destWorld.z, facingAngle, duration / 1000.0f);
|
|
|
|
|
|
|
|
|
|
if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_)
|
|
|
|
|
owner_.creatureMoveCallback_(moverGuid, destWorld.x, destWorld.y, destWorld.z, duration);
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("SMSG_MONSTER_MOVE_TRANSPORT: mover=0x", std::hex, moverGuid,
|
|
|
|
|
" transport=0x", transportGuid, std::dec,
|
|
|
|
|
" dur=", duration, "ms dest=(", destWorld.x, ",", destWorld.y, ",", destWorld.z, ")");
|
|
|
|
|
} else {
|
|
|
|
|
glm::vec3 startWorld = owner_.transportManager_->getPlayerWorldPosition(transportGuid, startLocalCanonical);
|
|
|
|
|
owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, startLocalCanonical, false, 0.0f);
|
|
|
|
|
entity->setPosition(startWorld.x, startWorld.y, startWorld.z, facingAngle);
|
|
|
|
|
if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_)
|
|
|
|
|
owner_.creatureMoveCallback_(moverGuid, startWorld.x, startWorld.y, startWorld.z, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Teleport Handlers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleTeleportAck(network::Packet& packet) {
|
|
|
|
|
const bool taTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < (taTbc ? 8u : 4u)) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint64_t guid = taTbc
|
|
|
|
|
? packet.readUInt64() : packet.readPackedGuid();
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(4)) return;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
uint32_t counter = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
const bool taNoFlags2 = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
|
|
|
|
const size_t minMoveSz = taNoFlags2 ? (4 + 4 + 4 * 4) : (4 + 2 + 4 + 4 * 4);
|
2026-03-29 20:53:26 -07:00
|
|
|
if (packet.getRemainingSize() < minMoveSz) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
packet.readUInt32(); // moveFlags
|
|
|
|
|
if (!taNoFlags2)
|
|
|
|
|
packet.readUInt16(); // moveFlags2 (WotLK only)
|
|
|
|
|
uint32_t moveTime = packet.readUInt32();
|
|
|
|
|
float serverX = packet.readFloat();
|
|
|
|
|
float serverY = packet.readFloat();
|
|
|
|
|
float serverZ = packet.readFloat();
|
|
|
|
|
float orientation = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
|
|
|
|
|
" counter=", counter,
|
|
|
|
|
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")");
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
|
|
|
|
movementInfo.x = canonical.x;
|
|
|
|
|
movementInfo.y = canonical.y;
|
|
|
|
|
movementInfo.z = canonical.z;
|
|
|
|
|
movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation);
|
|
|
|
|
movementInfo.flags = 0;
|
|
|
|
|
|
2026-03-28 11:35:10 +03:00
|
|
|
// Clear cast bar on teleport — SpellHandler owns the casting_ flag
|
|
|
|
|
if (owner_.spellHandler_) owner_.spellHandler_->resetCastState();
|
|
|
|
|
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (owner_.socket) {
|
|
|
|
|
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK));
|
|
|
|
|
const bool legacyGuidAck =
|
|
|
|
|
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
|
|
|
|
|
if (legacyGuidAck) {
|
|
|
|
|
ack.writeUInt64(owner_.playerGuid);
|
|
|
|
|
} else {
|
|
|
|
|
ack.writePackedGuid(owner_.playerGuid);
|
|
|
|
|
}
|
|
|
|
|
ack.writeUInt32(counter);
|
|
|
|
|
ack.writeUInt32(moveTime);
|
|
|
|
|
owner_.socket->send(ack);
|
|
|
|
|
LOG_INFO("Sent MSG_MOVE_TELEPORT_ACK response");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.worldEntryCallback_) {
|
|
|
|
|
owner_.worldEntryCallback_(owner_.currentMapId_, serverX, serverY, serverZ, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleNewWorld(network::Packet& packet) {
|
2026-03-29 20:53:26 -07:00
|
|
|
if (!packet.hasRemaining(20)) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
LOG_WARNING("SMSG_NEW_WORLD too short");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t mapId = packet.readUInt32();
|
|
|
|
|
float serverX = packet.readFloat();
|
|
|
|
|
float serverY = packet.readFloat();
|
|
|
|
|
float serverZ = packet.readFloat();
|
|
|
|
|
float orientation = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_NEW_WORLD: mapId=", mapId,
|
|
|
|
|
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
|
|
|
|
" orient=", orientation);
|
|
|
|
|
|
|
|
|
|
const bool isSameMap = (mapId == owner_.currentMapId_);
|
|
|
|
|
const bool isResurrection = owner_.resurrectPending_;
|
|
|
|
|
if (isSameMap && isResurrection) {
|
|
|
|
|
LOG_INFO("SMSG_NEW_WORLD same-map resurrection — skipping world reload");
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
|
|
|
|
movementInfo.x = canonical.x;
|
|
|
|
|
movementInfo.y = canonical.y;
|
|
|
|
|
movementInfo.z = canonical.z;
|
|
|
|
|
movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation);
|
|
|
|
|
movementInfo.flags = 0;
|
|
|
|
|
movementInfo.flags2 = 0;
|
|
|
|
|
|
|
|
|
|
owner_.resurrectPending_ = false;
|
|
|
|
|
owner_.resurrectRequestPending_ = false;
|
|
|
|
|
owner_.releasedSpirit_ = false;
|
|
|
|
|
owner_.playerDead_ = false;
|
|
|
|
|
owner_.repopPending_ = false;
|
|
|
|
|
owner_.pendingSpiritHealerGuid_ = 0;
|
|
|
|
|
owner_.resurrectCasterGuid_ = 0;
|
|
|
|
|
owner_.corpseMapId_ = 0;
|
|
|
|
|
owner_.corpseGuid_ = 0;
|
|
|
|
|
owner_.clearHostileAttackers();
|
|
|
|
|
owner_.stopAutoAttack();
|
|
|
|
|
owner_.tabCycleStale = true;
|
2026-03-28 11:35:10 +03:00
|
|
|
owner_.resetCastState();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
|
|
|
|
|
owner_.socket->send(ack);
|
|
|
|
|
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK (resurrection)");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner_.currentMapId_ = mapId;
|
|
|
|
|
owner_.inInstance_ = false;
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
owner_.socket->tracePacketsFor(std::chrono::seconds(12), "new_world");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
|
|
|
|
movementInfo.x = canonical.x;
|
|
|
|
|
movementInfo.y = canonical.y;
|
|
|
|
|
movementInfo.z = canonical.z;
|
|
|
|
|
movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation);
|
|
|
|
|
movementInfo.flags = 0;
|
|
|
|
|
movementInfo.flags2 = 0;
|
|
|
|
|
serverMovementAllowed_ = true;
|
|
|
|
|
owner_.resurrectPending_ = false;
|
|
|
|
|
owner_.resurrectRequestPending_ = false;
|
|
|
|
|
onTaxiFlight_ = false;
|
|
|
|
|
taxiMountActive_ = false;
|
|
|
|
|
taxiActivatePending_ = false;
|
|
|
|
|
taxiClientActive_ = false;
|
|
|
|
|
taxiClientPath_.clear();
|
|
|
|
|
taxiRecoverPending_ = false;
|
|
|
|
|
taxiStartGrace_ = 0.0f;
|
|
|
|
|
owner_.currentMountDisplayId_ = 0;
|
|
|
|
|
taxiMountDisplayId_ = 0;
|
|
|
|
|
if (owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (guid == owner_.playerGuid) continue;
|
|
|
|
|
if (entity->getType() == ObjectType::UNIT && owner_.creatureDespawnCallback_) {
|
|
|
|
|
owner_.creatureDespawnCallback_(guid);
|
|
|
|
|
} else if (entity->getType() == ObjectType::PLAYER && owner_.playerDespawnCallback_) {
|
|
|
|
|
owner_.playerDespawnCallback_(guid);
|
|
|
|
|
} else if (entity->getType() == ObjectType::GAMEOBJECT && owner_.gameObjectDespawnCallback_) {
|
|
|
|
|
owner_.gameObjectDespawnCallback_(guid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
owner_.otherPlayerVisibleItemEntries_.clear();
|
|
|
|
|
owner_.otherPlayerVisibleDirty_.clear();
|
|
|
|
|
otherPlayerMoveTimeMs_.clear();
|
2026-03-29 16:49:17 -07:00
|
|
|
if (owner_.spellHandler_) owner_.spellHandler_->clearUnitCastStates();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
owner_.unitAurasCache_.clear();
|
|
|
|
|
owner_.clearCombatText();
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
owner_.getEntityManager().clear();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
owner_.clearHostileAttackers();
|
|
|
|
|
owner_.worldStates_.clear();
|
|
|
|
|
owner_.gossipPois_.clear();
|
|
|
|
|
owner_.worldStateMapId_ = mapId;
|
|
|
|
|
owner_.worldStateZoneId_ = 0;
|
|
|
|
|
owner_.activeAreaTriggers_.clear();
|
|
|
|
|
owner_.areaTriggerCheckTimer_ = -5.0f;
|
|
|
|
|
owner_.areaTriggerSuppressFirst_ = true;
|
|
|
|
|
owner_.stopAutoAttack();
|
2026-03-28 11:35:10 +03:00
|
|
|
owner_.resetCastState();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
|
|
|
|
|
owner_.socket->send(ack);
|
|
|
|
|
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner_.timeSinceLastPing = 0.0f;
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
LOG_WARNING("World transfer keepalive: sending immediate ping after MSG_MOVE_WORLDPORT_ACK");
|
|
|
|
|
owner_.sendPing();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.worldEntryCallback_) {
|
|
|
|
|
owner_.worldEntryCallback_(mapId, serverX, serverY, serverZ, isSameMap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.addonEventCallback_) {
|
|
|
|
|
owner_.addonEventCallback_("PLAYER_ENTERING_WORLD", {"0"});
|
|
|
|
|
owner_.addonEventCallback_("ZONE_CHANGED_NEW_AREA", {});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Taxi / Flight Path Handlers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::loadTaxiDbc() {
|
|
|
|
|
if (taxiDbcLoaded_) return;
|
|
|
|
|
taxiDbcLoaded_ = true;
|
|
|
|
|
|
[refactor] Break Application::getInstance() from GameHandler
Introduce `GameServices` struct — an explicit dependency bundle that
`Application` populates and passes to `GameHandler` at construction time.
Eliminates all 47 hidden `Application::getInstance()` calls in
`src/game/*.cpp`, completing SOLID-D (dependency-inversion) cleanup.
Changes:
- New `include/game/game_services.hpp` — `struct GameServices` carrying
pointers to `Renderer`, `AssetManager`, `ExpansionRegistry`, and two
taxi-mount display IDs
- `GameHandler(GameServices&)` replaces default constructor; exposes
`services() const` accessor for domain handlers
- `Application` holds `game::GameServices gameServices_`; populates it
after all subsystems are created, then constructs `GameHandler`
(fixes latent init-order bug: `GameHandler` was previously created
before `AssetManager` / `ExpansionRegistry`)
- `game_handler.cpp`: duplicate `isActiveExpansion` / `isClassicLikeExpansion` /
`isPreWotlk` anonymous-namespace helpers removed; `game_utils.hpp`
included instead
- All domain handlers (`InventoryHandler`, `SpellHandler`, `MovementHandler`,
`CombatHandler`, `QuestHandler`, `SocialHandler`, `WardenHandler`) replace
`Application::getInstance().getXxx()` with `owner_.services().xxx`
2026-03-30 09:17:42 +03:00
|
|
|
auto* am = owner_.services().assetManager;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!am || !am->isInitialized()) return;
|
|
|
|
|
|
|
|
|
|
auto nodesDbc = am->loadDBC("TaxiNodes.dbc");
|
|
|
|
|
if (nodesDbc && nodesDbc->isLoaded()) {
|
|
|
|
|
const auto* tnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiNodes") : nullptr;
|
|
|
|
|
// Cache field indices before the loop
|
|
|
|
|
const uint32_t tnIdField = tnL ? (*tnL)["ID"] : 0;
|
|
|
|
|
const uint32_t tnMapField = tnL ? (*tnL)["MapID"] : 1;
|
|
|
|
|
const uint32_t tnXField = tnL ? (*tnL)["X"] : 2;
|
|
|
|
|
const uint32_t tnYField = tnL ? (*tnL)["Y"] : 3;
|
|
|
|
|
const uint32_t tnZField = tnL ? (*tnL)["Z"] : 4;
|
|
|
|
|
const uint32_t tnNameField = tnL ? (*tnL)["Name"] : 5;
|
|
|
|
|
const uint32_t mountAllianceField = tnL ? (*tnL)["MountDisplayIdAlliance"] : 22;
|
|
|
|
|
const uint32_t mountHordeField = tnL ? (*tnL)["MountDisplayIdHorde"] : 23;
|
|
|
|
|
const uint32_t mountAllianceFB = tnL ? (*tnL)["MountDisplayIdAllianceFallback"] : 20;
|
|
|
|
|
const uint32_t mountHordeFB = tnL ? (*tnL)["MountDisplayIdHordeFallback"] : 21;
|
|
|
|
|
uint32_t fieldCount = nodesDbc->getFieldCount();
|
|
|
|
|
for (uint32_t i = 0; i < nodesDbc->getRecordCount(); i++) {
|
|
|
|
|
TaxiNode node;
|
|
|
|
|
node.id = nodesDbc->getUInt32(i, tnIdField);
|
|
|
|
|
node.mapId = nodesDbc->getUInt32(i, tnMapField);
|
|
|
|
|
node.x = nodesDbc->getFloat(i, tnXField);
|
|
|
|
|
node.y = nodesDbc->getFloat(i, tnYField);
|
|
|
|
|
node.z = nodesDbc->getFloat(i, tnZField);
|
|
|
|
|
node.name = nodesDbc->getString(i, tnNameField);
|
|
|
|
|
if (fieldCount > mountHordeField) {
|
|
|
|
|
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceField);
|
|
|
|
|
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeField);
|
|
|
|
|
if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount > mountHordeFB) {
|
|
|
|
|
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceFB);
|
|
|
|
|
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeFB);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
uint32_t nodeId = node.id;
|
|
|
|
|
if (nodeId > 0) {
|
|
|
|
|
taxiNodes_[nodeId] = std::move(node);
|
|
|
|
|
}
|
|
|
|
|
if (nodeId == 195) {
|
|
|
|
|
std::string fields;
|
|
|
|
|
for (uint32_t f = 0; f < fieldCount; f++) {
|
|
|
|
|
fields += std::to_string(f) + ":" + std::to_string(nodesDbc->getUInt32(i, f)) + " ";
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("TaxiNodes[195] fields: ", fields);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Loaded ", taxiNodes_.size(), " taxi nodes from TaxiNodes.dbc");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("Could not load TaxiNodes.dbc");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto pathDbc = am->loadDBC("TaxiPath.dbc");
|
|
|
|
|
if (pathDbc && pathDbc->isLoaded()) {
|
|
|
|
|
const auto* tpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPath") : nullptr;
|
|
|
|
|
const uint32_t tpIdField = tpL ? (*tpL)["ID"] : 0;
|
|
|
|
|
const uint32_t tpFromField = tpL ? (*tpL)["FromNode"] : 1;
|
|
|
|
|
const uint32_t tpToField = tpL ? (*tpL)["ToNode"] : 2;
|
|
|
|
|
const uint32_t tpCostField = tpL ? (*tpL)["Cost"] : 3;
|
|
|
|
|
for (uint32_t i = 0; i < pathDbc->getRecordCount(); i++) {
|
|
|
|
|
TaxiPathEdge edge;
|
|
|
|
|
edge.pathId = pathDbc->getUInt32(i, tpIdField);
|
|
|
|
|
edge.fromNode = pathDbc->getUInt32(i, tpFromField);
|
|
|
|
|
edge.toNode = pathDbc->getUInt32(i, tpToField);
|
|
|
|
|
edge.cost = pathDbc->getUInt32(i, tpCostField);
|
|
|
|
|
taxiPathEdges_.push_back(edge);
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Loaded ", taxiPathEdges_.size(), " taxi path edges from TaxiPath.dbc");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("Could not load TaxiPath.dbc");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto pathNodeDbc = am->loadDBC("TaxiPathNode.dbc");
|
|
|
|
|
if (pathNodeDbc && pathNodeDbc->isLoaded()) {
|
|
|
|
|
const auto* tpnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPathNode") : nullptr;
|
|
|
|
|
const uint32_t tpnIdField = tpnL ? (*tpnL)["ID"] : 0;
|
|
|
|
|
const uint32_t tpnPathField = tpnL ? (*tpnL)["PathID"] : 1;
|
|
|
|
|
const uint32_t tpnIndexField = tpnL ? (*tpnL)["NodeIndex"] : 2;
|
|
|
|
|
const uint32_t tpnMapField = tpnL ? (*tpnL)["MapID"] : 3;
|
|
|
|
|
const uint32_t tpnXField = tpnL ? (*tpnL)["X"] : 4;
|
|
|
|
|
const uint32_t tpnYField = tpnL ? (*tpnL)["Y"] : 5;
|
|
|
|
|
const uint32_t tpnZField = tpnL ? (*tpnL)["Z"] : 6;
|
|
|
|
|
for (uint32_t i = 0; i < pathNodeDbc->getRecordCount(); i++) {
|
|
|
|
|
TaxiPathNode node;
|
|
|
|
|
node.id = pathNodeDbc->getUInt32(i, tpnIdField);
|
|
|
|
|
node.pathId = pathNodeDbc->getUInt32(i, tpnPathField);
|
|
|
|
|
node.nodeIndex = pathNodeDbc->getUInt32(i, tpnIndexField);
|
|
|
|
|
node.mapId = pathNodeDbc->getUInt32(i, tpnMapField);
|
|
|
|
|
node.x = pathNodeDbc->getFloat(i, tpnXField);
|
|
|
|
|
node.y = pathNodeDbc->getFloat(i, tpnYField);
|
|
|
|
|
node.z = pathNodeDbc->getFloat(i, tpnZField);
|
|
|
|
|
taxiPathNodes_[node.pathId].push_back(node);
|
|
|
|
|
}
|
|
|
|
|
for (auto& [pathId, nodes] : taxiPathNodes_) {
|
|
|
|
|
std::sort(nodes.begin(), nodes.end(),
|
|
|
|
|
[](const TaxiPathNode& a, const TaxiPathNode& b) {
|
|
|
|
|
return a.nodeIndex < b.nodeIndex;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Loaded ", pathNodeDbc->getRecordCount(), " taxi path waypoints from TaxiPathNode.dbc");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("Could not load TaxiPathNode.dbc");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleShowTaxiNodes(network::Packet& packet) {
|
|
|
|
|
ShowTaxiNodesData data;
|
|
|
|
|
if (!ShowTaxiNodesParser::parse(packet, data)) {
|
|
|
|
|
LOG_WARNING("Failed to parse SMSG_SHOWTAXINODES");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadTaxiDbc();
|
|
|
|
|
|
|
|
|
|
if (taxiMaskInitialized_) {
|
|
|
|
|
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
|
|
|
|
uint32_t newBits = data.nodeMask[i] & ~knownTaxiMask_[i];
|
|
|
|
|
if (newBits == 0) continue;
|
|
|
|
|
for (uint32_t bit = 0; bit < 32; ++bit) {
|
|
|
|
|
if (newBits & (1u << bit)) {
|
|
|
|
|
uint32_t nodeId = i * 32 + bit + 1;
|
|
|
|
|
auto it = taxiNodes_.find(nodeId);
|
|
|
|
|
if (it != taxiNodes_.end()) {
|
|
|
|
|
owner_.addSystemChatMessage("Discovered flight path: " + it->second.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) {
|
|
|
|
|
knownTaxiMask_[i] = data.nodeMask[i];
|
|
|
|
|
}
|
|
|
|
|
taxiMaskInitialized_ = true;
|
|
|
|
|
|
|
|
|
|
currentTaxiData_ = data;
|
|
|
|
|
taxiNpcGuid_ = data.npcGuid;
|
|
|
|
|
taxiWindowOpen_ = true;
|
2026-03-28 14:36:14 -07:00
|
|
|
owner_.closeGossip();
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
buildTaxiCostMap();
|
|
|
|
|
auto it = taxiNodes_.find(data.nearestNode);
|
|
|
|
|
if (it != taxiNodes_.end()) {
|
|
|
|
|
LOG_INFO("Taxi node ", data.nearestNode, " mounts: A=", it->second.mountDisplayIdAlliance,
|
|
|
|
|
" H=", it->second.mountDisplayIdHorde);
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Taxi window opened, nearest node=", data.nearestNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::applyTaxiMountForCurrentNode() {
|
|
|
|
|
if (taxiMountActive_ || !owner_.mountCallback_) return;
|
|
|
|
|
auto it = taxiNodes_.find(currentTaxiData_.nearestNode);
|
|
|
|
|
if (it == taxiNodes_.end()) {
|
|
|
|
|
bool isAlliance = true;
|
|
|
|
|
switch (owner_.playerRace_) {
|
|
|
|
|
case Race::ORC: case Race::UNDEAD: case Race::TAUREN: case Race::TROLL:
|
|
|
|
|
case Race::GOBLIN: case Race::BLOOD_ELF:
|
|
|
|
|
isAlliance = false; break;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
uint32_t mountId = isAlliance ? 1210u : 1310u;
|
|
|
|
|
taxiMountDisplayId_ = mountId;
|
|
|
|
|
taxiMountActive_ = true;
|
|
|
|
|
LOG_INFO("Taxi mount fallback (node ", currentTaxiData_.nearestNode, " not in DBC): displayId=", mountId);
|
|
|
|
|
owner_.mountCallback_(mountId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isAlliance = true;
|
|
|
|
|
switch (owner_.playerRace_) {
|
|
|
|
|
case Race::ORC:
|
|
|
|
|
case Race::UNDEAD:
|
|
|
|
|
case Race::TAUREN:
|
|
|
|
|
case Race::TROLL:
|
|
|
|
|
case Race::GOBLIN:
|
|
|
|
|
case Race::BLOOD_ELF:
|
|
|
|
|
isAlliance = false;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
isAlliance = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
uint32_t mountId = isAlliance ? it->second.mountDisplayIdAlliance
|
|
|
|
|
: it->second.mountDisplayIdHorde;
|
|
|
|
|
if (mountId == 541) mountId = 0;
|
|
|
|
|
if (mountId == 0) {
|
|
|
|
|
mountId = isAlliance ? it->second.mountDisplayIdHorde
|
|
|
|
|
: it->second.mountDisplayIdAlliance;
|
|
|
|
|
if (mountId == 541) mountId = 0;
|
|
|
|
|
}
|
|
|
|
|
if (mountId == 0) {
|
[refactor] Break Application::getInstance() from GameHandler
Introduce `GameServices` struct — an explicit dependency bundle that
`Application` populates and passes to `GameHandler` at construction time.
Eliminates all 47 hidden `Application::getInstance()` calls in
`src/game/*.cpp`, completing SOLID-D (dependency-inversion) cleanup.
Changes:
- New `include/game/game_services.hpp` — `struct GameServices` carrying
pointers to `Renderer`, `AssetManager`, `ExpansionRegistry`, and two
taxi-mount display IDs
- `GameHandler(GameServices&)` replaces default constructor; exposes
`services() const` accessor for domain handlers
- `Application` holds `game::GameServices gameServices_`; populates it
after all subsystems are created, then constructs `GameHandler`
(fixes latent init-order bug: `GameHandler` was previously created
before `AssetManager` / `ExpansionRegistry`)
- `game_handler.cpp`: duplicate `isActiveExpansion` / `isClassicLikeExpansion` /
`isPreWotlk` anonymous-namespace helpers removed; `game_utils.hpp`
included instead
- All domain handlers (`InventoryHandler`, `SpellHandler`, `MovementHandler`,
`CombatHandler`, `QuestHandler`, `SocialHandler`, `WardenHandler`) replace
`Application::getInstance().getXxx()` with `owner_.services().xxx`
2026-03-30 09:17:42 +03:00
|
|
|
uint32_t gryphonId = owner_.services().gryphonDisplayId;
|
|
|
|
|
uint32_t wyvernId = owner_.services().wyvernDisplayId;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (isAlliance && gryphonId != 0) mountId = gryphonId;
|
|
|
|
|
if (!isAlliance && wyvernId != 0) mountId = wyvernId;
|
|
|
|
|
if (mountId == 0) {
|
|
|
|
|
mountId = (isAlliance ? wyvernId : gryphonId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (mountId == 0) {
|
|
|
|
|
if (it->second.mountDisplayIdAlliance != 0) mountId = it->second.mountDisplayIdAlliance;
|
|
|
|
|
else if (it->second.mountDisplayIdHorde != 0) mountId = it->second.mountDisplayIdHorde;
|
|
|
|
|
}
|
|
|
|
|
if (mountId == 0) {
|
|
|
|
|
static const uint32_t kAllianceTaxiDisplays[] = {1210u, 1211u, 1212u, 1213u};
|
|
|
|
|
static const uint32_t kHordeTaxiDisplays[] = {1310u, 1311u, 1312u};
|
|
|
|
|
mountId = isAlliance ? kAllianceTaxiDisplays[0] : kHordeTaxiDisplays[0];
|
|
|
|
|
}
|
|
|
|
|
if (mountId == 0) {
|
|
|
|
|
mountId = isAlliance ? 30412u : 30413u;
|
|
|
|
|
}
|
|
|
|
|
if (mountId != 0) {
|
|
|
|
|
taxiMountDisplayId_ = mountId;
|
|
|
|
|
taxiMountActive_ = true;
|
|
|
|
|
LOG_INFO("Taxi mount apply: displayId=", mountId);
|
|
|
|
|
owner_.mountCallback_(mountId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
|
|
|
|
|
taxiClientPath_.clear();
|
|
|
|
|
taxiClientIndex_ = 0;
|
|
|
|
|
taxiClientActive_ = false;
|
|
|
|
|
taxiClientSegmentProgress_ = 0.0f;
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i + 1 < pathNodes.size(); i++) {
|
|
|
|
|
uint32_t fromNode = pathNodes[i];
|
|
|
|
|
uint32_t toNode = pathNodes[i + 1];
|
|
|
|
|
uint32_t pathId = 0;
|
|
|
|
|
for (const auto& edge : taxiPathEdges_) {
|
|
|
|
|
if (edge.fromNode == fromNode && edge.toNode == toNode) {
|
|
|
|
|
pathId = edge.pathId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (pathId == 0) {
|
|
|
|
|
LOG_WARNING("No taxi path found from node ", fromNode, " to ", toNode);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
auto pathIt = taxiPathNodes_.find(pathId);
|
|
|
|
|
if (pathIt != taxiPathNodes_.end()) {
|
|
|
|
|
for (const auto& wpNode : pathIt->second) {
|
|
|
|
|
glm::vec3 serverPos(wpNode.x, wpNode.y, wpNode.z);
|
|
|
|
|
glm::vec3 canonical = core::coords::serverToCanonical(serverPos);
|
|
|
|
|
taxiClientPath_.push_back(canonical);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("No spline waypoints found for taxi pathId ", pathId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (taxiClientPath_.size() < 2) {
|
|
|
|
|
taxiClientPath_.clear();
|
|
|
|
|
for (uint32_t nodeId : pathNodes) {
|
|
|
|
|
auto nodeIt = taxiNodes_.find(nodeId);
|
|
|
|
|
if (nodeIt == taxiNodes_.end()) continue;
|
|
|
|
|
glm::vec3 serverPos(nodeIt->second.x, nodeIt->second.y, nodeIt->second.z);
|
|
|
|
|
taxiClientPath_.push_back(core::coords::serverToCanonical(serverPos));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (taxiClientPath_.size() < 2) {
|
|
|
|
|
LOG_WARNING("Taxi path too short: ", taxiClientPath_.size(), " waypoints");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 start = taxiClientPath_[0];
|
|
|
|
|
glm::vec3 dir(0.0f);
|
|
|
|
|
float dirLenSq = 0.0f;
|
|
|
|
|
for (size_t i = 1; i < taxiClientPath_.size(); i++) {
|
|
|
|
|
dir = taxiClientPath_[i] - start;
|
|
|
|
|
dirLenSq = glm::dot(dir, dir);
|
|
|
|
|
if (dirLenSq >= 1e-6f) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float initialOrientation = movementInfo.orientation;
|
|
|
|
|
float initialRenderYaw = movementInfo.orientation;
|
|
|
|
|
float initialPitch = 0.0f;
|
|
|
|
|
float initialRoll = 0.0f;
|
|
|
|
|
if (dirLenSq >= 1e-6f) {
|
|
|
|
|
initialOrientation = std::atan2(dir.y, dir.x);
|
|
|
|
|
glm::vec3 renderDir = core::coords::canonicalToRender(dir);
|
|
|
|
|
initialRenderYaw = std::atan2(renderDir.y, renderDir.x);
|
|
|
|
|
glm::vec3 dirNorm = dir * glm::inversesqrt(dirLenSq);
|
|
|
|
|
initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
movementInfo.x = start.x;
|
|
|
|
|
movementInfo.y = start.y;
|
|
|
|
|
movementInfo.z = start.z;
|
|
|
|
|
movementInfo.orientation = initialOrientation;
|
|
|
|
|
sanitizeMovementForTaxi();
|
|
|
|
|
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto playerEntity = owner_.getEntityManager().getEntity(owner_.playerGuid);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (playerEntity) {
|
|
|
|
|
playerEntity->setPosition(start.x, start.y, start.z, initialOrientation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.taxiOrientationCallback_) {
|
|
|
|
|
owner_.taxiOrientationCallback_(initialRenderYaw, initialPitch, initialRoll);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Taxi flight started with ", taxiClientPath_.size(), " spline waypoints");
|
|
|
|
|
taxiClientActive_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::updateClientTaxi(float deltaTime) {
|
|
|
|
|
if (!taxiClientActive_ || taxiClientPath_.size() < 2) return;
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto playerEntity = owner_.getEntityManager().getEntity(owner_.playerGuid);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
auto finishTaxiFlight = [&]() {
|
|
|
|
|
if (!taxiClientPath_.empty()) {
|
|
|
|
|
const auto& landingPos = taxiClientPath_.back();
|
|
|
|
|
if (playerEntity) {
|
|
|
|
|
playerEntity->setPosition(landingPos.x, landingPos.y, landingPos.z,
|
|
|
|
|
movementInfo.orientation);
|
|
|
|
|
}
|
|
|
|
|
movementInfo.x = landingPos.x;
|
|
|
|
|
movementInfo.y = landingPos.y;
|
|
|
|
|
movementInfo.z = landingPos.z;
|
|
|
|
|
LOG_INFO("Taxi landing: snapped to final waypoint (",
|
|
|
|
|
landingPos.x, ", ", landingPos.y, ", ", landingPos.z, ")");
|
|
|
|
|
}
|
|
|
|
|
taxiClientActive_ = false;
|
|
|
|
|
onTaxiFlight_ = false;
|
|
|
|
|
taxiLandingCooldown_ = 2.0f;
|
|
|
|
|
if (taxiMountActive_ && owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
taxiMountActive_ = false;
|
|
|
|
|
taxiMountDisplayId_ = 0;
|
|
|
|
|
owner_.currentMountDisplayId_ = 0;
|
|
|
|
|
taxiClientPath_.clear();
|
|
|
|
|
taxiRecoverPending_ = false;
|
|
|
|
|
movementInfo.flags = 0;
|
|
|
|
|
movementInfo.flags2 = 0;
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_STOP);
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Taxi flight landed (client path)");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) {
|
|
|
|
|
finishTaxiFlight();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float remainingDistance = taxiClientSegmentProgress_ + (taxiClientSpeed_ * deltaTime);
|
|
|
|
|
glm::vec3 start(0.0f);
|
|
|
|
|
glm::vec3 end(0.0f);
|
|
|
|
|
glm::vec3 dir(0.0f);
|
|
|
|
|
float segmentLen = 0.0f;
|
|
|
|
|
float t = 0.0f;
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) {
|
|
|
|
|
finishTaxiFlight();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start = taxiClientPath_[taxiClientIndex_];
|
|
|
|
|
end = taxiClientPath_[taxiClientIndex_ + 1];
|
|
|
|
|
dir = end - start;
|
|
|
|
|
float segLenSq = glm::dot(dir, dir);
|
|
|
|
|
|
|
|
|
|
if (segLenSq < 1e-4f) {
|
|
|
|
|
taxiClientIndex_++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
segmentLen = std::sqrt(segLenSq);
|
|
|
|
|
|
|
|
|
|
if (remainingDistance >= segmentLen) {
|
|
|
|
|
remainingDistance -= segmentLen;
|
|
|
|
|
taxiClientIndex_++;
|
|
|
|
|
taxiClientSegmentProgress_ = 0.0f;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
taxiClientSegmentProgress_ = remainingDistance;
|
|
|
|
|
t = taxiClientSegmentProgress_ / segmentLen;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 p0 = (taxiClientIndex_ > 0) ? taxiClientPath_[taxiClientIndex_ - 1] : start;
|
|
|
|
|
glm::vec3 p1 = start;
|
|
|
|
|
glm::vec3 p2 = end;
|
|
|
|
|
glm::vec3 p3 = (taxiClientIndex_ + 2 < taxiClientPath_.size()) ?
|
|
|
|
|
taxiClientPath_[taxiClientIndex_ + 2] : end;
|
|
|
|
|
|
|
|
|
|
float t2 = t * t;
|
|
|
|
|
float t3 = t2 * t;
|
|
|
|
|
glm::vec3 nextPos = 0.5f * (
|
|
|
|
|
(2.0f * p1) +
|
|
|
|
|
(-p0 + p2) * t +
|
|
|
|
|
(2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 +
|
|
|
|
|
(-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
glm::vec3 tangent = 0.5f * (
|
|
|
|
|
(-p0 + p2) +
|
|
|
|
|
2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t +
|
|
|
|
|
3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2
|
|
|
|
|
);
|
|
|
|
|
float tangentLenSq = glm::dot(tangent, tangent);
|
|
|
|
|
if (tangentLenSq < 1e-8f) {
|
|
|
|
|
tangent = dir;
|
|
|
|
|
tangentLenSq = glm::dot(tangent, tangent);
|
|
|
|
|
if (tangentLenSq < 1e-8f) {
|
|
|
|
|
tangent = glm::vec3(std::cos(movementInfo.orientation), std::sin(movementInfo.orientation), 0.0f);
|
|
|
|
|
tangentLenSq = 1.0f; // unit vector
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float targetOrientation = std::atan2(tangent.y, tangent.x);
|
|
|
|
|
|
|
|
|
|
glm::vec3 tangentNorm = tangent * glm::inversesqrt(std::max(tangentLenSq, 1e-8f));
|
|
|
|
|
float pitch = std::asin(std::clamp(tangentNorm.z, -1.0f, 1.0f));
|
|
|
|
|
|
|
|
|
|
float currentOrientation = movementInfo.orientation;
|
|
|
|
|
float orientDiff = targetOrientation - currentOrientation;
|
|
|
|
|
while (orientDiff > 3.14159265f) orientDiff -= 6.28318530f;
|
|
|
|
|
while (orientDiff < -3.14159265f) orientDiff += 6.28318530f;
|
|
|
|
|
float roll = -orientDiff * 2.5f;
|
|
|
|
|
roll = std::clamp(roll, -0.7f, 0.7f);
|
|
|
|
|
|
|
|
|
|
float smoothOrientation = currentOrientation + orientDiff * std::min(1.0f, deltaTime * 3.0f);
|
|
|
|
|
|
|
|
|
|
if (playerEntity) {
|
|
|
|
|
playerEntity->setPosition(nextPos.x, nextPos.y, nextPos.z, smoothOrientation);
|
|
|
|
|
}
|
|
|
|
|
movementInfo.x = nextPos.x;
|
|
|
|
|
movementInfo.y = nextPos.y;
|
|
|
|
|
movementInfo.z = nextPos.z;
|
|
|
|
|
movementInfo.orientation = smoothOrientation;
|
|
|
|
|
|
|
|
|
|
if (owner_.taxiOrientationCallback_) {
|
|
|
|
|
glm::vec3 renderTangent = core::coords::canonicalToRender(tangent);
|
|
|
|
|
float renderYaw = std::atan2(renderTangent.y, renderTangent.x);
|
|
|
|
|
owner_.taxiOrientationCallback_(renderYaw, pitch, roll);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::handleActivateTaxiReply(network::Packet& packet) {
|
|
|
|
|
ActivateTaxiReplyData data;
|
|
|
|
|
if (!ActivateTaxiReplyParser::parse(packet, data)) {
|
|
|
|
|
LOG_WARNING("Failed to parse SMSG_ACTIVATETAXIREPLY");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!taxiActivatePending_) {
|
|
|
|
|
LOG_DEBUG("Ignoring stray taxi reply: result=", data.result);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.result == 0) {
|
|
|
|
|
if (onTaxiFlight_ && !taxiActivatePending_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onTaxiFlight_ = true;
|
|
|
|
|
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
|
|
|
|
|
sanitizeMovementForTaxi();
|
|
|
|
|
taxiWindowOpen_ = false;
|
|
|
|
|
taxiActivatePending_ = false;
|
|
|
|
|
taxiActivateTimer_ = 0.0f;
|
|
|
|
|
applyTaxiMountForCurrentNode();
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Taxi flight started!");
|
|
|
|
|
} else {
|
|
|
|
|
if (onTaxiFlight_ || taxiClientActive_) {
|
|
|
|
|
LOG_WARNING("Ignoring stale taxi failure reply while flight is active: result=", data.result);
|
|
|
|
|
taxiActivatePending_ = false;
|
|
|
|
|
taxiActivateTimer_ = 0.0f;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
LOG_WARNING("Taxi activation failed, result=", data.result);
|
|
|
|
|
owner_.addSystemChatMessage("Cannot take that flight path.");
|
|
|
|
|
taxiActivatePending_ = false;
|
|
|
|
|
taxiActivateTimer_ = 0.0f;
|
|
|
|
|
if (taxiMountActive_ && owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
taxiMountActive_ = false;
|
|
|
|
|
taxiMountDisplayId_ = 0;
|
|
|
|
|
onTaxiFlight_ = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::closeTaxi() {
|
|
|
|
|
taxiWindowOpen_ = false;
|
|
|
|
|
|
|
|
|
|
if (taxiActivatePending_ || onTaxiFlight_ || taxiClientActive_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (taxiMountActive_ && owner_.mountCallback_) {
|
|
|
|
|
owner_.mountCallback_(0);
|
|
|
|
|
}
|
|
|
|
|
taxiMountActive_ = false;
|
|
|
|
|
taxiMountDisplayId_ = 0;
|
|
|
|
|
|
|
|
|
|
taxiActivatePending_ = false;
|
|
|
|
|
onTaxiFlight_ = false;
|
|
|
|
|
|
|
|
|
|
taxiLandingCooldown_ = 2.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::buildTaxiCostMap() {
|
|
|
|
|
taxiCostMap_.clear();
|
|
|
|
|
uint32_t startNode = currentTaxiData_.nearestNode;
|
|
|
|
|
if (startNode == 0) return;
|
|
|
|
|
|
|
|
|
|
struct AdjEntry { uint32_t node; uint32_t cost; };
|
|
|
|
|
std::unordered_map<uint32_t, std::vector<AdjEntry>> adj;
|
|
|
|
|
for (const auto& edge : taxiPathEdges_) {
|
|
|
|
|
adj[edge.fromNode].push_back({edge.toNode, edge.cost});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::deque<uint32_t> queue;
|
|
|
|
|
queue.push_back(startNode);
|
|
|
|
|
taxiCostMap_[startNode] = 0;
|
|
|
|
|
|
|
|
|
|
while (!queue.empty()) {
|
|
|
|
|
uint32_t cur = queue.front();
|
|
|
|
|
queue.pop_front();
|
|
|
|
|
for (const auto& next : adj[cur]) {
|
|
|
|
|
if (taxiCostMap_.find(next.node) == taxiCostMap_.end()) {
|
|
|
|
|
taxiCostMap_[next.node] = taxiCostMap_[cur] + next.cost;
|
|
|
|
|
queue.push_back(next.node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t MovementHandler::getTaxiCostTo(uint32_t destNodeId) const {
|
|
|
|
|
auto it = taxiCostMap_.find(destNodeId);
|
|
|
|
|
return (it != taxiCostMap_.end()) ? it->second : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::activateTaxi(uint32_t destNodeId) {
|
|
|
|
|
if (!owner_.socket || owner_.state != WorldState::IN_WORLD) return;
|
|
|
|
|
|
|
|
|
|
if (taxiActivatePending_ || onTaxiFlight_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t startNode = currentTaxiData_.nearestNode;
|
|
|
|
|
if (startNode == 0 || destNodeId == 0 || startNode == destNodeId) return;
|
|
|
|
|
|
|
|
|
|
if (owner_.isMounted()) {
|
|
|
|
|
LOG_INFO("Taxi activate: dismounting current mount");
|
|
|
|
|
if (owner_.mountCallback_) owner_.mountCallback_(0);
|
|
|
|
|
owner_.currentMountDisplayId_ = 0;
|
|
|
|
|
dismount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
auto destIt = taxiNodes_.find(destNodeId);
|
|
|
|
|
if (destIt != taxiNodes_.end() && !destIt->second.name.empty()) {
|
|
|
|
|
taxiDestName_ = destIt->second.name;
|
|
|
|
|
owner_.addSystemChatMessage("Requesting flight to " + destIt->second.name + "...");
|
|
|
|
|
} else {
|
|
|
|
|
taxiDestName_.clear();
|
|
|
|
|
owner_.addSystemChatMessage("Taxi: requesting flight...");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BFS to find path from startNode to destNodeId
|
|
|
|
|
std::unordered_map<uint32_t, std::vector<uint32_t>> adj;
|
|
|
|
|
for (const auto& edge : taxiPathEdges_) {
|
|
|
|
|
adj[edge.fromNode].push_back(edge.toNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_map<uint32_t, uint32_t> parent;
|
|
|
|
|
std::deque<uint32_t> queue;
|
|
|
|
|
queue.push_back(startNode);
|
|
|
|
|
parent[startNode] = startNode;
|
|
|
|
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
while (!queue.empty()) {
|
|
|
|
|
uint32_t cur = queue.front();
|
|
|
|
|
queue.pop_front();
|
|
|
|
|
if (cur == destNodeId) { found = true; break; }
|
|
|
|
|
for (uint32_t next : adj[cur]) {
|
|
|
|
|
if (parent.find(next) == parent.end()) {
|
|
|
|
|
parent[next] = cur;
|
|
|
|
|
queue.push_back(next);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
|
LOG_WARNING("No taxi path found from node ", startNode, " to ", destNodeId);
|
|
|
|
|
owner_.addSystemChatMessage("No flight path available to that destination.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<uint32_t> path;
|
|
|
|
|
for (uint32_t n = destNodeId; n != startNode; n = parent[n]) {
|
|
|
|
|
path.push_back(n);
|
|
|
|
|
}
|
|
|
|
|
path.push_back(startNode);
|
|
|
|
|
std::reverse(path.begin(), path.end());
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Taxi path: ", path.size(), " nodes, from ", startNode, " to ", destNodeId);
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Taxi activate: npc=0x", std::hex, taxiNpcGuid_, std::dec,
|
|
|
|
|
" start=", startNode, " dest=", destNodeId, " pathLen=", path.size());
|
|
|
|
|
if (!path.empty()) {
|
|
|
|
|
std::string pathStr;
|
|
|
|
|
for (size_t i = 0; i < path.size(); i++) {
|
|
|
|
|
pathStr += std::to_string(path[i]);
|
|
|
|
|
if (i + 1 < path.size()) pathStr += "->";
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("Taxi path nodes: ", pathStr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t totalCost = getTaxiCostTo(destNodeId);
|
|
|
|
|
LOG_INFO("Taxi activate: start=", startNode, " dest=", destNodeId, " cost=", totalCost);
|
|
|
|
|
|
|
|
|
|
auto basicPkt = ActivateTaxiPacket::build(taxiNpcGuid_, startNode, destNodeId);
|
|
|
|
|
owner_.socket->send(basicPkt);
|
|
|
|
|
|
|
|
|
|
taxiWindowOpen_ = false;
|
|
|
|
|
taxiActivatePending_ = true;
|
|
|
|
|
taxiActivateTimer_ = 0.0f;
|
|
|
|
|
taxiStartGrace_ = 2.0f;
|
|
|
|
|
if (!onTaxiFlight_) {
|
|
|
|
|
onTaxiFlight_ = true;
|
|
|
|
|
sanitizeMovementForTaxi();
|
|
|
|
|
applyTaxiMountForCurrentNode();
|
|
|
|
|
}
|
|
|
|
|
if (owner_.socket) {
|
|
|
|
|
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.taxiPrecacheCallback_) {
|
|
|
|
|
std::vector<glm::vec3> previewPath;
|
|
|
|
|
for (size_t i = 0; i + 1 < path.size(); i++) {
|
|
|
|
|
uint32_t fromNode = path[i];
|
|
|
|
|
uint32_t toNode = path[i + 1];
|
|
|
|
|
uint32_t pathId = 0;
|
|
|
|
|
for (const auto& edge : taxiPathEdges_) {
|
|
|
|
|
if (edge.fromNode == fromNode && edge.toNode == toNode) {
|
|
|
|
|
pathId = edge.pathId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (pathId == 0) continue;
|
|
|
|
|
auto pathIt = taxiPathNodes_.find(pathId);
|
|
|
|
|
if (pathIt != taxiPathNodes_.end()) {
|
|
|
|
|
for (const auto& wpNode : pathIt->second) {
|
|
|
|
|
glm::vec3 serverPos(wpNode.x, wpNode.y, wpNode.z);
|
|
|
|
|
glm::vec3 canonical = core::coords::serverToCanonical(serverPos);
|
|
|
|
|
previewPath.push_back(canonical);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (previewPath.size() >= 2) {
|
|
|
|
|
owner_.taxiPrecacheCallback_(previewPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.taxiFlightStartCallback_) {
|
|
|
|
|
owner_.taxiFlightStartCallback_();
|
|
|
|
|
}
|
|
|
|
|
startClientTaxiPath(path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Area Trigger Detection (moved from GameHandler)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::loadAreaTriggerDbc() {
|
|
|
|
|
if (owner_.areaTriggerDbcLoaded_) return;
|
|
|
|
|
owner_.areaTriggerDbcLoaded_ = true;
|
|
|
|
|
|
[refactor] Break Application::getInstance() from GameHandler
Introduce `GameServices` struct — an explicit dependency bundle that
`Application` populates and passes to `GameHandler` at construction time.
Eliminates all 47 hidden `Application::getInstance()` calls in
`src/game/*.cpp`, completing SOLID-D (dependency-inversion) cleanup.
Changes:
- New `include/game/game_services.hpp` — `struct GameServices` carrying
pointers to `Renderer`, `AssetManager`, `ExpansionRegistry`, and two
taxi-mount display IDs
- `GameHandler(GameServices&)` replaces default constructor; exposes
`services() const` accessor for domain handlers
- `Application` holds `game::GameServices gameServices_`; populates it
after all subsystems are created, then constructs `GameHandler`
(fixes latent init-order bug: `GameHandler` was previously created
before `AssetManager` / `ExpansionRegistry`)
- `game_handler.cpp`: duplicate `isActiveExpansion` / `isClassicLikeExpansion` /
`isPreWotlk` anonymous-namespace helpers removed; `game_utils.hpp`
included instead
- All domain handlers (`InventoryHandler`, `SpellHandler`, `MovementHandler`,
`CombatHandler`, `QuestHandler`, `SocialHandler`, `WardenHandler`) replace
`Application::getInstance().getXxx()` with `owner_.services().xxx`
2026-03-30 09:17:42 +03:00
|
|
|
auto* am = owner_.services().assetManager;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!am || !am->isInitialized()) return;
|
|
|
|
|
|
|
|
|
|
auto dbc = am->loadDBC("AreaTrigger.dbc");
|
|
|
|
|
if (!dbc || !dbc->isLoaded()) {
|
|
|
|
|
LOG_WARNING("Failed to load AreaTrigger.dbc");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner_.areaTriggers_.reserve(dbc->getRecordCount());
|
|
|
|
|
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
|
|
|
|
GameHandler::AreaTriggerEntry at;
|
|
|
|
|
at.id = dbc->getUInt32(i, 0);
|
|
|
|
|
at.mapId = dbc->getUInt32(i, 1);
|
|
|
|
|
// DBC stores positions in server/wire format (X=west, Y=north) — swap to canonical
|
|
|
|
|
at.x = dbc->getFloat(i, 3); // canonical X (north) = DBC field 3 (Y_wire)
|
|
|
|
|
at.y = dbc->getFloat(i, 2); // canonical Y (west) = DBC field 2 (X_wire)
|
|
|
|
|
at.z = dbc->getFloat(i, 4);
|
|
|
|
|
at.radius = dbc->getFloat(i, 5);
|
|
|
|
|
at.boxLength = dbc->getFloat(i, 6);
|
|
|
|
|
at.boxWidth = dbc->getFloat(i, 7);
|
|
|
|
|
at.boxHeight = dbc->getFloat(i, 8);
|
|
|
|
|
at.boxYaw = dbc->getFloat(i, 9);
|
|
|
|
|
owner_.areaTriggers_.push_back(at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_WARNING("Loaded ", owner_.areaTriggers_.size(), " area triggers from AreaTrigger.dbc");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::checkAreaTriggers() {
|
|
|
|
|
if (!owner_.isInWorld()) return;
|
|
|
|
|
if (onTaxiFlight_ || taxiClientActive_) return;
|
|
|
|
|
|
|
|
|
|
loadAreaTriggerDbc();
|
|
|
|
|
if (owner_.areaTriggers_.empty()) return;
|
|
|
|
|
|
|
|
|
|
const float px = movementInfo.x;
|
|
|
|
|
const float py = movementInfo.y;
|
|
|
|
|
const float pz = movementInfo.z;
|
|
|
|
|
|
fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.
WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.
Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00
|
|
|
// Sanity: if position is near map origin on Eastern Kingdoms (map 0),
|
|
|
|
|
// something has corrupted movementInfo — skip area trigger check to
|
|
|
|
|
// avoid firing Alterac/Hillsbrad triggers and causing a rogue teleport.
|
|
|
|
|
if (owner_.currentMapId_ == 0 && std::abs(px) < 500.0f && std::abs(py) < 500.0f) {
|
|
|
|
|
LOG_WARNING("checkAreaTriggers: position near map origin (", px, ", ", py, ", ", pz,
|
|
|
|
|
") on map 0 — skipping to avoid rogue teleport. onTransport=",
|
|
|
|
|
owner_.isOnTransport(), " transportGuid=0x", std::hex,
|
|
|
|
|
owner_.playerTransportGuid_, std::dec);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
// On first check after map transfer, just mark which triggers we're inside
|
|
|
|
|
// without firing them — prevents exit portal from immediately sending us back
|
|
|
|
|
bool suppressFirst = owner_.areaTriggerSuppressFirst_;
|
|
|
|
|
if (suppressFirst) {
|
|
|
|
|
owner_.areaTriggerSuppressFirst_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto& at : owner_.areaTriggers_) {
|
|
|
|
|
if (at.mapId != owner_.currentMapId_) continue;
|
|
|
|
|
|
|
|
|
|
bool inside = false;
|
|
|
|
|
if (at.radius > 0.0f) {
|
2026-04-05 02:35:47 -07:00
|
|
|
// Sphere trigger — use actual DBC radius
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
float dx = px - at.x;
|
|
|
|
|
float dy = py - at.y;
|
|
|
|
|
float dz = pz - at.z;
|
|
|
|
|
float distSq = dx * dx + dy * dy + dz * dz;
|
2026-04-05 02:35:47 -07:00
|
|
|
inside = (distSq <= at.radius * at.radius);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
|
2026-04-05 02:35:47 -07:00
|
|
|
// Box trigger — use actual DBC dimensions
|
|
|
|
|
float effLength = at.boxLength;
|
|
|
|
|
float effWidth = at.boxWidth;
|
|
|
|
|
float effHeight = at.boxHeight;
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
|
|
|
|
|
float dx = px - at.x;
|
|
|
|
|
float dy = py - at.y;
|
|
|
|
|
float dz = pz - at.z;
|
|
|
|
|
|
|
|
|
|
// Rotate into box-local space
|
|
|
|
|
float cosYaw = std::cos(-at.boxYaw);
|
|
|
|
|
float sinYaw = std::sin(-at.boxYaw);
|
|
|
|
|
float localX = dx * cosYaw - dy * sinYaw;
|
|
|
|
|
float localY = dx * sinYaw + dy * cosYaw;
|
|
|
|
|
|
|
|
|
|
inside = (std::abs(localX) <= effLength * 0.5f &&
|
|
|
|
|
std::abs(localY) <= effWidth * 0.5f &&
|
|
|
|
|
std::abs(dz) <= effHeight * 0.5f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (inside) {
|
|
|
|
|
if (owner_.activeAreaTriggers_.count(at.id) == 0) {
|
|
|
|
|
owner_.activeAreaTriggers_.insert(at.id);
|
|
|
|
|
|
|
|
|
|
if (suppressFirst) {
|
|
|
|
|
// After map transfer: mark triggers we're inside of, but don't fire them.
|
|
|
|
|
// This prevents the exit portal from immediately sending us back.
|
|
|
|
|
LOG_WARNING("AreaTrigger suppressed (post-transfer): AT", at.id);
|
|
|
|
|
} else {
|
2026-04-05 04:40:46 -07:00
|
|
|
// Send CMSG_AREATRIGGER — the player is already inside the
|
|
|
|
|
// trigger radius so the server's distance check should pass.
|
|
|
|
|
// Do NOT spoof position to the trigger center; that tells the
|
|
|
|
|
// server we're somewhere we're not and can cause rogue teleports.
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER));
|
|
|
|
|
pkt.writeUInt32(at.id);
|
|
|
|
|
owner_.socket->send(pkt);
|
2026-04-05 04:40:46 -07:00
|
|
|
LOG_DEBUG("Fired CMSG_AREATRIGGER: id=", at.id);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Player left the trigger — allow re-fire on re-entry
|
|
|
|
|
owner_.activeAreaTriggers_.erase(at.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Transport Attachments (moved from GameHandler)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid,
|
|
|
|
|
const glm::vec3& localOffset, bool hasLocalOrientation,
|
|
|
|
|
float localOrientation) {
|
|
|
|
|
if (childGuid == 0 || transportGuid == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameHandler::TransportAttachment& attachment = owner_.transportAttachments_[childGuid];
|
|
|
|
|
attachment.type = type;
|
|
|
|
|
attachment.transportGuid = transportGuid;
|
|
|
|
|
attachment.localOffset = localOffset;
|
|
|
|
|
attachment.hasLocalOrientation = hasLocalOrientation;
|
|
|
|
|
attachment.localOrientation = localOrientation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::clearTransportAttachment(uint64_t childGuid) {
|
|
|
|
|
if (childGuid == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
owner_.transportAttachments_.erase(childGuid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::updateAttachedTransportChildren(float /*deltaTime*/) {
|
|
|
|
|
if (!owner_.transportManager_ || owner_.transportAttachments_.empty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constexpr float kPosEpsilonSq = 0.0001f;
|
|
|
|
|
constexpr float kOriEpsilon = 0.001f;
|
|
|
|
|
std::vector<uint64_t> stale;
|
|
|
|
|
stale.reserve(8);
|
|
|
|
|
|
|
|
|
|
for (const auto& [childGuid, attachment] : owner_.transportAttachments_) {
|
refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).
What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())
8 opcodes re-registered by EntityController::registerOpcodes():
SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
SMSG_PAGE_TEXT_QUERY_RESPONSE
Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.
Also included:
- .clang-tidy: add modernize-use-nodiscard,
modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs
Line counts:
entity_controller.hpp 147 lines (new)
entity_controller.cpp 2172 lines (new)
game_handler.cpp 8095 lines (was 10143, −2048)
Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
|
|
|
auto entity = owner_.getEntityManager().getEntity(childGuid);
|
refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
|
|
|
if (!entity) {
|
|
|
|
|
stale.push_back(childGuid);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActiveTransport* transport = owner_.transportManager_->getTransport(attachment.transportGuid);
|
|
|
|
|
if (!transport) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition(
|
|
|
|
|
attachment.transportGuid, attachment.localOffset);
|
|
|
|
|
|
|
|
|
|
float composedOrientation = entity->getOrientation();
|
|
|
|
|
if (attachment.hasLocalOrientation) {
|
|
|
|
|
float baseYaw = transport->hasServerYaw ? transport->serverYaw : 0.0f;
|
|
|
|
|
composedOrientation = baseYaw + attachment.localOrientation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 oldPos(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
float oldOrientation = entity->getOrientation();
|
|
|
|
|
glm::vec3 delta = composed - oldPos;
|
|
|
|
|
const bool positionChanged = glm::dot(delta, delta) > kPosEpsilonSq;
|
|
|
|
|
const bool orientationChanged = std::abs(composedOrientation - oldOrientation) > kOriEpsilon;
|
|
|
|
|
if (!positionChanged && !orientationChanged) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entity->setPosition(composed.x, composed.y, composed.z, composedOrientation);
|
|
|
|
|
|
|
|
|
|
if (attachment.type == ObjectType::UNIT) {
|
|
|
|
|
if (owner_.creatureMoveCallback_) {
|
|
|
|
|
owner_.creatureMoveCallback_(childGuid, composed.x, composed.y, composed.z, 0);
|
|
|
|
|
}
|
|
|
|
|
} else if (attachment.type == ObjectType::GAMEOBJECT) {
|
|
|
|
|
if (owner_.gameObjectMoveCallback_) {
|
|
|
|
|
owner_.gameObjectMoveCallback_(childGuid, composed.x, composed.y, composed.z, composedOrientation);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (uint64_t guid : stale) {
|
|
|
|
|
owner_.transportAttachments_.erase(guid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Follow target (moved from GameHandler)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void MovementHandler::followTarget() {
|
|
|
|
|
if (owner_.state != WorldState::IN_WORLD) {
|
|
|
|
|
LOG_WARNING("Cannot follow: not in world");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner_.targetGuid == 0) {
|
|
|
|
|
owner_.addSystemChatMessage("You must target someone to follow.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto target = owner_.getTarget();
|
|
|
|
|
if (!target) {
|
|
|
|
|
owner_.addSystemChatMessage("Invalid target.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set follow target
|
|
|
|
|
owner_.followTargetGuid_ = owner_.targetGuid;
|
|
|
|
|
|
|
|
|
|
// Initialize render-space position from entity's canonical coords
|
|
|
|
|
owner_.followRenderPos_ = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
|
|
|
|
|
|
|
|
|
// Tell camera controller to start auto-following
|
|
|
|
|
if (owner_.autoFollowCallback_) {
|
|
|
|
|
owner_.autoFollowCallback_(&owner_.followRenderPos_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get target name
|
|
|
|
|
std::string targetName = "Target";
|
|
|
|
|
if (target->getType() == ObjectType::PLAYER) {
|
|
|
|
|
auto player = std::static_pointer_cast<Player>(target);
|
|
|
|
|
if (!player->getName().empty()) {
|
|
|
|
|
targetName = player->getName();
|
|
|
|
|
}
|
|
|
|
|
} else if (target->getType() == ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<Unit>(target);
|
|
|
|
|
targetName = unit->getName();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner_.addSystemChatMessage("Now following " + targetName + ".");
|
|
|
|
|
LOG_INFO("Following target: ", targetName, " (GUID: 0x", std::hex, owner_.targetGuid, std::dec, ")");
|
|
|
|
|
owner_.fireAddonEvent("AUTOFOLLOW_BEGIN", {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MovementHandler::cancelFollow() {
|
|
|
|
|
if (owner_.followTargetGuid_ == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
owner_.followTargetGuid_ = 0;
|
|
|
|
|
if (owner_.autoFollowCallback_) {
|
|
|
|
|
owner_.autoFollowCallback_(nullptr);
|
|
|
|
|
}
|
|
|
|
|
owner_.addSystemChatMessage("You stop following.");
|
|
|
|
|
owner_.fireAddonEvent("AUTOFOLLOW_END", {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace game
|
|
|
|
|
} // namespace wowee
|