Kelsidavis-WoWee/src/game/movement_handler.cpp
Kelsi 4e0e234ae9 fix: MSG_MOVE_START_DESCEND never set DESCENDING flag
Only ASCENDING was cleared — the DESCENDING flag was never toggled,
so outgoing movement packets during flight descent had incorrect flags.
Also clears DESCENDING on start-ascend and stop-ascend for symmetry.

Replaces static heartbeat log counter with member variable (was shared
across instances and not thread-safe) and demotes to LOG_DEBUG.
2026-03-29 18:28:49 -07:00

2872 lines
119 KiB
C++

#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) {
if (packet.getSize() - packet.getReadPos() >= 1)
(void)packet.readPackedGuid();
};
}
// Spline move: synth flags (each opcode produces different flags)
{
auto makeSynthHandler = [this](uint32_t synthFlags) {
return [this, synthFlags](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
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);
}
// 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;
};
};
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_);
// 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
};
// 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(); };
// ---- 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.
if (owner_.isCasting() && !owner_.isChanneling()) {
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);
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::DESCENDING);
break;
case Opcode::MSG_MOVE_STOP_ASCEND:
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ASCENDING);
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::DESCENDING);
break;
case Opcode::MSG_MOVE_START_DESCEND:
// 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).
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ASCENDING);
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::DESCENDING);
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) {
if (owner_.transportManager_) {
glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition(owner_.playerTransportGuid_, owner_.playerTransportOffset_);
movementInfo.x = composed.x;
movementInfo.y = composed.y;
movementInfo.z = composed.z;
}
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" : ""));
// 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;
// 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, ")");
}
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;
owner_.resurrectPending_ = false;
owner_.resurrectRequestPending_ = false;
owner_.selfResAvailable_ = false;
owner_.playerDead_ = false;
owner_.releasedSpirit_ = false;
owner_.corpseGuid_ = 0;
owner_.corpseReclaimAvailableMs_ = 0;
owner_.repopPending_ = false;
owner_.pendingSpiritHealerGuid_ = 0;
owner_.resurrectCasterGuid_ = 0;
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 {
for (const auto& a : owner_.getPlayerAuras()) {
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
// ============================================================
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();
size_t remaining = packet.getSize() - packet.getReadPos();
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;
if (owner_.socket) {
network::Packet ack(wireOpcode(ackOpcode));
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
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);
}
ack.writeFloat(newSpeed);
owner_.socket->send(ack);
}
if (std::isnan(newSpeed) || newSpeed < 0.1f || newSpeed > 100.0f) {
LOG_WARNING("Ignoring invalid ", name, " speed: ", newSpeed);
return;
}
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");
if (packet.getSize() - packet.getReadPos() < (rootTbc ? 8u : 2u)) return;
uint64_t guid = rootTbc
? packet.readUInt64() : packet.readPackedGuid();
if (packet.getSize() - packet.getReadPos() < 4) return;
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;
uint16_t ackWire = wireOpcode(rooted ? Opcode::CMSG_FORCE_MOVE_ROOT_ACK
: Opcode::CMSG_FORCE_MOVE_UNROOT_ACK);
if (ackWire == 0xFFFF) return;
network::Packet ack(ackWire);
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
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);
owner_.socket->send(ack);
}
void MovementHandler::handleForceMoveFlagChange(network::Packet& packet, const char* name,
Opcode ackOpcode, uint32_t flag, bool set) {
const bool fmfTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (fmfTbcLike ? 8u : 2u)) return;
uint64_t guid = fmfTbcLike
? packet.readUInt64() : packet.readPackedGuid();
if (packet.getSize() - packet.getReadPos() < 4) return;
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;
uint16_t ackWire = wireOpcode(ackOpcode);
if (ackWire == 0xFFFF) return;
network::Packet ack(ackWire);
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
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);
owner_.socket->send(ack);
}
void MovementHandler::handleMoveSetCollisionHeight(network::Packet& packet) {
const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (legacyGuid ? 8u : 2u)) return;
uint64_t guid = legacyGuid ? packet.readUInt64() : packet.readPackedGuid();
if (packet.getSize() - packet.getReadPos() < 8) return;
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;
uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK);
if (ackWire == 0xFFFF) return;
network::Packet ack(ackWire);
const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
ack.writeUInt64(owner_.playerGuid);
} else {
ack.writePackedGuid(owner_.playerGuid);
}
ack.writeUInt32(counter);
MovementInfo wire = movementInfo;
wire.time = nextMovementTimestampMs();
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 (owner_.packetParsers_) owner_.packetParsers_->writeMovementPayload(ack, wire);
else MovementPacket::writeMovementPayload(ack, wire);
ack.writeFloat(height);
owner_.socket->send(ack);
}
void MovementHandler::handleMoveKnockBack(network::Packet& packet) {
const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (mkbTbc ? 8u : 2u)) return;
uint64_t guid = mkbTbc
? packet.readUInt64() : packet.readPackedGuid();
if (packet.getSize() - packet.getReadPos() < 20) return;
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;
uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK);
if (ackWire == 0xFFFF) return;
network::Packet ack(ackWire);
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
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);
owner_.socket->send(ack);
}
// ============================================================
// 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();
const size_t remaining = packet.getSize() - packet.getReadPos();
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();
}
}
}
auto entity = owner_.getEntityManager().getEntity(moverGuid);
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;
}
}
auto entity = owner_.getEntityManager().getEntity(data.guid);
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) {
auto target = owner_.getEntityManager().getEntity(data.facingTarget);
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) {
auto target = owner_.getEntityManager().getEntity(data.facingTarget);
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) {
if (packet.getSize() - packet.getReadPos() < 8 + 1 + 8 + 12) return;
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();
auto entity = owner_.getEntityManager().getEntity(moverGuid);
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();
if (auto tgt = owner_.getEntityManager().getEntity(tgtGuid)) {
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");
if (packet.getSize() - packet.getReadPos() < (taTbc ? 8u : 4u)) {
LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short");
return;
}
uint64_t guid = taTbc
? packet.readUInt64() : packet.readPackedGuid();
if (packet.getSize() - packet.getReadPos() < 4) return;
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);
if (packet.getSize() - packet.getReadPos() < minMoveSz) {
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;
// Clear cast bar on teleport — SpellHandler owns the casting_ flag
if (owner_.spellHandler_) owner_.spellHandler_->resetCastState();
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) {
if (packet.getSize() - packet.getReadPos() < 20) {
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;
owner_.resetCastState();
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);
}
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
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();
if (owner_.spellHandler_) owner_.spellHandler_->clearUnitCastStates();
owner_.unitAurasCache_.clear();
owner_.clearCombatText();
owner_.getEntityManager().clear();
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();
owner_.resetCastState();
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;
auto* am = core::Application::getInstance().getAssetManager();
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;
owner_.closeGossip();
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) {
auto& app = core::Application::getInstance();
uint32_t gryphonId = app.getGryphonDisplayId();
uint32_t wyvernId = app.getWyvernDisplayId();
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();
auto playerEntity = owner_.getEntityManager().getEntity(owner_.playerGuid);
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;
auto playerEntity = owner_.getEntityManager().getEntity(owner_.playerGuid);
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;
auto* am = core::Application::getInstance().getAssetManager();
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;
// 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) {
// Sphere trigger — use actual radius, with small floor for very tiny triggers
float effectiveRadius = std::max(at.radius, 3.0f);
float dx = px - at.x;
float dy = py - at.y;
float dz = pz - at.z;
float distSq = dx * dx + dy * dy + dz * dz;
inside = (distSq <= effectiveRadius * effectiveRadius);
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
// Box trigger — use actual size, with small floor for tiny triggers
float boxMin = 4.0f;
float effLength = std::max(at.boxLength, boxMin);
float effWidth = std::max(at.boxWidth, boxMin);
float effHeight = std::max(at.boxHeight, boxMin);
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 {
// Temporarily move player to trigger center so the server's distance
// check passes, then restore to actual position so the server doesn't
// persist the fake position on disconnect.
float savedX = movementInfo.x, savedY = movementInfo.y, savedZ = movementInfo.z;
movementInfo.x = at.x;
movementInfo.y = at.y;
movementInfo.z = at.z;
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER));
pkt.writeUInt32(at.id);
owner_.socket->send(pkt);
LOG_WARNING("Fired CMSG_AREATRIGGER: id=", at.id,
" at (", at.x, ", ", at.y, ", ", at.z, ")");
// Restore actual player position
movementInfo.x = savedX;
movementInfo.y = savedY;
movementInfo.z = savedZ;
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
}
}
} 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_) {
auto entity = owner_.getEntityManager().getEntity(childGuid);
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