Kelsidavis-WoWee/src/game/movement_handler.cpp
Kelsi e32f4fbff9 Merge master into chore/god-object-decomposition-2nd
Resolve conflicts:
- audio_callback_handler.cpp: keep PR's animation_controller include
- movement_handler.cpp: use PR accessors with master's transportResolved logic
- world_packets.cpp: keep PR's decomposed version (functions moved to split files)

Apply overkill field fix to world_packets_entity.cpp (WotLK
SMSG_ATTACKERSTATEUPDATE missing uint32 overkill between damage and
subDamageCount).
2026-04-05 19:42:25 -07:00

2790 lines
118 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.movementInfoRef()) {}
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.hasRemaining(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.hasRemaining(1)) return;
uint64_t guid = packet.readPackedGuid();
if (guid == 0 || guid == owner_.getPlayerGuid() || !owner_.unitMoveFlagsCallbackRef()) return;
owner_.unitMoveFlagsCallbackRef()(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_.getPlayerGuid() && 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_.getPlayerGuid() || !owner_.unitMoveFlagsCallbackRef()) return;
owner_.unitMoveFlagsCallbackRef()(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_.getPlayerGuid()) {
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_.getState() != WorldState::IN_WORLD) {
LOG_WARNING("Cannot send movement in state: ", (int)owner_.getState());
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_.resurrectPendingRef() && !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_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("PLAYER_STARTED_MOVING", {});
else if (!isMoving && wasMoving && owner_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("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_.getTransportManager()) {
if (auto* tr = owner_.getTransportManager()->getTransport(owner_.playerTransportGuidRef()); tr && tr->isM2) {
includeTransportInWire = false;
}
}
// Add transport data if player is on a server-recognized transport
if (includeTransportInWire) {
bool transportResolved = false;
if (owner_.getTransportManager()) {
auto* tr = owner_.getTransportManager()->getTransport(owner_.playerTransportGuidRef());
if (tr) {
transportResolved = true;
glm::vec3 composed = owner_.getTransportManager()->getPlayerWorldPosition(owner_.playerTransportGuidRef(), owner_.playerTransportOffsetRef());
movementInfo.x = composed.x;
movementInfo.y = composed.y;
movementInfo.z = composed.z;
}
}
if (!transportResolved) {
// Transport not tracked — don't send ONTRANSPORT to the server.
// Sending stale transport GUID + local offset causes the server to
// compute a bad world position and teleport us to map origin.
LOG_WARNING("sendMovement: transport 0x", std::hex, owner_.playerTransportGuidRef(),
std::dec, " not found — clearing transport state");
includeTransportInWire = false;
owner_.clearPlayerTransport();
}
}
if (includeTransportInWire) {
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
movementInfo.transportGuid = owner_.playerTransportGuidRef();
movementInfo.transportX = owner_.playerTransportOffsetRef().x;
movementInfo.transportY = owner_.playerTransportOffsetRef().y;
movementInfo.transportZ = owner_.playerTransportOffsetRef().z;
movementInfo.transportTime = movementInfo.time;
movementInfo.transportSeat = -1;
movementInfo.transportTime2 = movementInfo.time;
float transportYawCanonical = 0.0f;
if (owner_.getTransportManager()) {
if (auto* tr = owner_.getTransportManager()->getTransport(owner_.playerTransportGuidRef()); 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" : ""));
// Detect near-origin position on Eastern Kingdoms (map 0) — this would place
// the player near Alterac Mountains and is almost certainly a bug.
if (owner_.getCurrentMapId() == 0 &&
std::abs(movementInfo.x) < 500.0f && std::abs(movementInfo.y) < 500.0f) {
LOG_WARNING("sendMovement: position near map origin! canonical=(",
movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z,
") onTransport=", owner_.isOnTransport(),
" transportGuid=0x", std::hex, owner_.playerTransportGuidRef(), std::dec,
" flags=0x", std::hex, movementInfo.flags, std::dec);
}
// 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 — log every ~60 heartbeats (~30s) to trace position drift.
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 60 == 0) {
LOG_WARNING("HEARTBEAT #", heartbeatLogCount_, " canonical=(",
movementInfo.x, ",", movementInfo.y, ",", movementInfo.z,
") server=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z,
") flags=0x", std::hex, movementInfo.flags, std::dec);
}
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_.getPacketParsers()
? owner_.getPacketParsers()->buildMovementPacket(opcode, wireInfo, owner_.getPlayerGuid())
: MovementPacket::build(opcode, wireInfo, owner_.getPlayerGuid());
owner_.getSocket()->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_.mountCallbackRef()) {
owner_.mountCallbackRef()(0);
}
taxiMountActive_ = false;
taxiMountDisplayId_ = 0;
owner_.currentMountDisplayIdRef() = 0;
owner_.vehicleIdRef() = 0;
// Death/resurrect state is intentionally NOT cleared here.
// Previously this method reset 10 death-related fields despite being named
// "forceClearTaxiAndMovementState", which could cancel pending resurrections
// or clear death state on taxi dismount. Death state is managed by
// entity_controller (markPlayerDead) and the resurrect packet handlers.
movementInfo.flags = 0;
movementInfo.flags2 = 0;
movementInfo.transportGuid = 0;
owner_.clearPlayerTransport();
if (owner_.getSocket() && owner_.getState() == 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_.getSocket()) return;
uint32_t savedMountAura = owner_.mountAuraSpellIdRef();
if (owner_.currentMountDisplayIdRef() != 0 || taxiMountActive_) {
if (owner_.mountCallbackRef()) {
owner_.mountCallbackRef()(0);
}
owner_.currentMountDisplayIdRef() = 0;
taxiMountActive_ = false;
taxiMountDisplayId_ = 0;
owner_.mountAuraSpellIdRef() = 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_.getSocket()->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
} else if (savedMountAura != 0) {
auto pkt = CancelAuraPacket::build(savedMountAura);
owner_.getSocket()->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_.getPlayerGuid()) {
auto pkt = CancelAuraPacket::build(a.spellId);
owner_.getSocket()->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_AURA (spell ", a.spellId, ") — brute force dismount");
}
}
}
}
// ============================================================
// Force Speed / Root / Flag Change Handlers
// ============================================================
// Shared force-ACK packet builder. All server-forced movement changes (speed,
// root, flag, collision-height, knockback) require the same ACK structure:
// GUID + counter + movement payload with server-space coordinates. Centralised
// here so transport coordinate conversion can't diverge between handlers.
network::Packet MovementHandler::buildForceAck(Opcode ackOpcode, uint32_t counter) {
network::Packet ack(wireOpcode(ackOpcode));
const bool legacyGuid =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuid) {
ack.writeUInt64(owner_.getPlayerGuid());
} else {
ack.writePackedGuid(owner_.getPlayerGuid());
}
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_.getPacketParsers()) {
owner_.getPacketParsers()->writeMovementPayload(ack, wire);
} else {
MovementPacket::writeMovementPayload(ack, wire);
}
return ack;
}
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.getRemainingSize();
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_.getPlayerGuid()) return;
// Validate BEFORE sending ACK — if we echo a bad speed back to the server
// but don't apply it locally, the client and server desync on movement speed.
if (std::isnan(newSpeed) || newSpeed < 0.1f || newSpeed > 100.0f) {
LOG_WARNING("Ignoring invalid ", name, " speed: ", newSpeed);
return;
}
if (owner_.getSocket()) {
auto ack = buildForceAck(ackOpcode, counter);
ack.writeFloat(newSpeed);
owner_.getSocket()->send(ack);
}
if (speedStorage) *speedStorage = newSpeed;
}
void MovementHandler::handleForceRunSpeedChange(network::Packet& packet) {
handleForceSpeedChange(packet, "RUN_SPEED", Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, &serverRunSpeed_);
if (!onTaxiFlight_ && !taxiMountActive_ && owner_.currentMountDisplayIdRef() != 0 && serverRunSpeed_ <= 8.5f) {
LOG_INFO("Auto-clearing mount from speed change: speed=", serverRunSpeed_,
" displayId=", owner_.currentMountDisplayIdRef());
owner_.currentMountDisplayIdRef() = 0;
if (owner_.mountCallbackRef()) {
owner_.mountCallbackRef()(0);
}
}
}
void MovementHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) {
const bool rootTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getRemainingSize() < (rootTbc ? 8u : 2u)) return;
uint64_t guid = rootTbc
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(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_.getPlayerGuid()) return;
if (rooted) {
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ROOT);
} else {
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ROOT);
}
if (!owner_.getSocket()) return;
Opcode ackOp = rooted ? Opcode::CMSG_FORCE_MOVE_ROOT_ACK : Opcode::CMSG_FORCE_MOVE_UNROOT_ACK;
if (wireOpcode(ackOp) == 0xFFFF) return;
owner_.getSocket()->send(buildForceAck(ackOp, counter));
}
void MovementHandler::handleForceMoveFlagChange(network::Packet& packet, const char* name,
Opcode ackOpcode, uint32_t flag, bool set) {
const bool fmfTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getRemainingSize() < (fmfTbcLike ? 8u : 2u)) return;
uint64_t guid = fmfTbcLike
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(4)) return;
uint32_t counter = packet.readUInt32();
LOG_INFO("SMSG_FORCE_", name, ": guid=0x", std::hex, guid, std::dec, " counter=", counter);
if (guid != owner_.getPlayerGuid()) return;
if (flag != 0) {
if (set) {
movementInfo.flags |= flag;
} else {
movementInfo.flags &= ~flag;
}
}
if (!owner_.getSocket()) return;
if (wireOpcode(ackOpcode) == 0xFFFF) return;
owner_.getSocket()->send(buildForceAck(ackOpcode, counter));
}
void MovementHandler::handleMoveSetCollisionHeight(network::Packet& packet) {
const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getRemainingSize() < (legacyGuid ? 8u : 2u)) return;
uint64_t guid = legacyGuid ? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(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_.getPlayerGuid()) return;
if (!owner_.getSocket()) return;
if (wireOpcode(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK) == 0xFFFF) return;
// buildForceAck now handles transport coordinate conversion, fixing the
// previous omission that caused desync when riding boats/zeppelins.
auto ack = buildForceAck(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK, counter);
ack.writeFloat(height);
owner_.getSocket()->send(ack);
}
void MovementHandler::handleMoveKnockBack(network::Packet& packet) {
const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getRemainingSize() < (mkbTbc ? 8u : 2u)) return;
uint64_t guid = mkbTbc
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(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_.getPlayerGuid()) return;
if (owner_.knockBackCallbackRef()) {
owner_.knockBackCallbackRef()(vcos, vsin, hspeed, vspeed);
}
if (!owner_.getSocket()) return;
if (wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK) == 0xFFFF) return;
owner_.getSocket()->send(buildForceAck(Opcode::CMSG_MOVE_KNOCK_BACK_ACK, counter));
}
// ============================================================
// 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.getRemainingSize();
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_.getPlayerGuid()) 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_.getPlayerGuid() || moverGuid == 0) {
return;
}
MovementInfo info = {};
info.flags = packet.readUInt32();
uint8_t flags2Size = owner_.getPacketParsers() ? owner_.getPacketParsers()->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_.getPacketParsers() ? owner_.getPacketParsers()->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_.getTransportManager()) {
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_.getTransportManager()->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_.creatureMoveCallbackRef()) {
const uint32_t notifyDuration = isStopOpcode ? 0u : durationMs;
owner_.creatureMoveCallbackRef()(moverGuid, canonical.x, canonical.y, canonical.z, notifyDuration);
}
if (owner_.unitAnimHintCallbackRef() && isJumpOpcode) {
owner_.unitAnimHintCallbackRef()(moverGuid, 38u);
}
if (owner_.unitMoveFlagsCallbackRef()) {
owner_.unitMoveFlagsCallbackRef()(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_.getState() == 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_.getPacketParsers()->parseMonsterMove(wrappedPacket, data)) {
parsed = true;
}
}
if (!parsed) {
network::Packet decompPacket(packet.getOpcode(), decompressed);
if (owner_.getPacketParsers()->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_.getPacketParsers()->parseMonsterMove(packet, data)) {
std::vector<uint8_t> stripped;
if (stripWrappedSubpacket(rawData, stripped)) {
network::Packet wrappedPacket(packet.getOpcode(), stripped);
if (owner_.getPacketParsers()->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_.creatureMoveCallbackRef()) {
owner_.creatureMoveCallbackRef()(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_.creatureMoveCallbackRef()) {
owner_.creatureMoveCallbackRef()(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_.creatureMoveCallbackRef()) {
owner_.creatureMoveCallbackRef()(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.getRemainingSize() < 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_.getTransportManager()) {
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_.getTransportManager()->getPlayerWorldPosition(transportGuid, localCanonical);
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallbackRef())
owner_.creatureMoveCallbackRef()(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
}
return;
}
/*uint32_t splineId =*/ packet.readUInt32();
uint8_t moveType = packet.readUInt8();
if (moveType == 1) {
if (owner_.getTransportManager()) {
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_.getTransportManager()->getPlayerWorldPosition(transportGuid, localCanonical);
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallbackRef())
owner_.creatureMoveCallbackRef()(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_.getTransportManager()) {
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_.getTransportManager()->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_.creatureMoveCallbackRef())
owner_.creatureMoveCallbackRef()(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_.getTransportManager()->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_.creatureMoveCallbackRef())
owner_.creatureMoveCallbackRef()(moverGuid, startWorld.x, startWorld.y, startWorld.z, 0);
}
}
// ============================================================
// Teleport Handlers
// ============================================================
void MovementHandler::handleTeleportAck(network::Packet& packet) {
const bool taTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getRemainingSize() < (taTbc ? 8u : 4u)) {
LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short");
return;
}
uint64_t guid = taTbc
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(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.getRemainingSize() < 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_WARNING("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
" counter=", counter,
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
" currentPos=(", movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z, ")");
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_.getSpellHandler()) owner_.getSpellHandler()->resetCastState();
if (owner_.getSocket()) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK));
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
ack.writeUInt64(owner_.getPlayerGuid());
} else {
ack.writePackedGuid(owner_.getPlayerGuid());
}
ack.writeUInt32(counter);
ack.writeUInt32(moveTime);
owner_.getSocket()->send(ack);
LOG_INFO("Sent MSG_MOVE_TELEPORT_ACK response");
}
if (owner_.worldEntryCallbackRef()) {
owner_.worldEntryCallbackRef()(owner_.currentMapIdRef(), serverX, serverY, serverZ, false);
}
}
void MovementHandler::handleNewWorld(network::Packet& packet) {
if (!packet.hasRemaining(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_.currentMapIdRef());
const bool isResurrection = owner_.resurrectPendingRef();
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_.resurrectPendingRef() = false;
owner_.resurrectRequestPendingRef() = false;
owner_.releasedSpiritRef() = false;
owner_.playerDeadRef() = false;
owner_.repopPendingRef() = false;
owner_.pendingSpiritHealerGuidRef() = 0;
owner_.resurrectCasterGuidRef() = 0;
owner_.corpseMapIdRef() = 0;
owner_.corpseGuidRef() = 0;
owner_.clearHostileAttackers();
owner_.stopAutoAttack();
owner_.tabCycleStaleRef() = true;
owner_.resetCastState();
if (owner_.getSocket()) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
owner_.getSocket()->send(ack);
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK (resurrection)");
}
return;
}
owner_.currentMapIdRef() = mapId;
owner_.inInstanceRef() = false;
if (owner_.getSocket()) {
owner_.getSocket()->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_.resurrectPendingRef() = false;
owner_.resurrectRequestPendingRef() = false;
onTaxiFlight_ = false;
taxiMountActive_ = false;
taxiActivatePending_ = false;
taxiClientActive_ = false;
taxiClientPath_.clear();
taxiRecoverPending_ = false;
taxiStartGrace_ = 0.0f;
owner_.currentMountDisplayIdRef() = 0;
taxiMountDisplayId_ = 0;
if (owner_.mountCallbackRef()) {
owner_.mountCallbackRef()(0);
}
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
if (guid == owner_.getPlayerGuid()) continue;
if (entity->getType() == ObjectType::UNIT && owner_.creatureDespawnCallbackRef()) {
owner_.creatureDespawnCallbackRef()(guid);
} else if (entity->getType() == ObjectType::PLAYER && owner_.playerDespawnCallbackRef()) {
owner_.playerDespawnCallbackRef()(guid);
} else if (entity->getType() == ObjectType::GAMEOBJECT && owner_.gameObjectDespawnCallbackRef()) {
owner_.gameObjectDespawnCallbackRef()(guid);
}
}
owner_.otherPlayerVisibleItemEntriesRef().clear();
owner_.otherPlayerVisibleDirtyRef().clear();
otherPlayerMoveTimeMs_.clear();
if (owner_.getSpellHandler()) owner_.getSpellHandler()->clearUnitCastStates();
owner_.unitAurasCacheRef().clear();
owner_.clearCombatText();
owner_.getEntityManager().clear();
owner_.clearHostileAttackers();
owner_.worldStatesRef().clear();
owner_.gossipPoisRef().clear();
owner_.worldStateMapIdRef() = mapId;
owner_.worldStateZoneIdRef() = 0;
owner_.activeAreaTriggersRef().clear();
owner_.areaTriggerCheckTimerRef() = -5.0f;
owner_.areaTriggerSuppressFirstRef() = true;
owner_.stopAutoAttack();
owner_.resetCastState();
if (owner_.getSocket()) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
owner_.getSocket()->send(ack);
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK");
}
owner_.timeSinceLastPingRef() = 0.0f;
if (owner_.getSocket()) {
LOG_WARNING("World transfer keepalive: sending immediate ping after MSG_MOVE_WORLDPORT_ACK");
owner_.sendPing();
}
if (owner_.worldEntryCallbackRef()) {
owner_.worldEntryCallbackRef()(mapId, serverX, serverY, serverZ, isSameMap);
}
if (owner_.addonEventCallbackRef()) {
owner_.addonEventCallbackRef()("PLAYER_ENTERING_WORLD", {"0"});
owner_.addonEventCallbackRef()("ZONE_CHANGED_NEW_AREA", {});
}
}
// ============================================================
// Taxi / Flight Path Handlers
// ============================================================
void MovementHandler::loadTaxiDbc() {
if (taxiDbcLoaded_) return;
taxiDbcLoaded_ = true;
auto* am = owner_.services().assetManager;
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_.mountCallbackRef()) return;
auto it = taxiNodes_.find(currentTaxiData_.nearestNode);
if (it == taxiNodes_.end()) {
bool isAlliance = true;
switch (owner_.playerRaceRef()) {
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_.mountCallbackRef()(mountId);
return;
}
bool isAlliance = true;
switch (owner_.playerRaceRef()) {
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) {
uint32_t gryphonId = owner_.services().gryphonDisplayId;
uint32_t wyvernId = owner_.services().wyvernDisplayId;
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_.mountCallbackRef()(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_.getPlayerGuid());
if (playerEntity) {
playerEntity->setPosition(start.x, start.y, start.z, initialOrientation);
}
if (owner_.taxiOrientationCallbackRef()) {
owner_.taxiOrientationCallbackRef()(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_.getPlayerGuid());
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_.mountCallbackRef()) {
owner_.mountCallbackRef()(0);
}
taxiMountActive_ = false;
taxiMountDisplayId_ = 0;
owner_.currentMountDisplayIdRef() = 0;
taxiClientPath_.clear();
taxiRecoverPending_ = false;
movementInfo.flags = 0;
movementInfo.flags2 = 0;
if (owner_.getSocket()) {
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_.taxiOrientationCallbackRef()) {
glm::vec3 renderTangent = core::coords::canonicalToRender(tangent);
float renderYaw = std::atan2(renderTangent.y, renderTangent.x);
owner_.taxiOrientationCallbackRef()(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_.getSocket()) {
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_.mountCallbackRef()) {
owner_.mountCallbackRef()(0);
}
taxiMountActive_ = false;
taxiMountDisplayId_ = 0;
onTaxiFlight_ = false;
}
}
void MovementHandler::closeTaxi() {
taxiWindowOpen_ = false;
if (taxiActivatePending_ || onTaxiFlight_ || taxiClientActive_) {
return;
}
if (taxiMountActive_ && owner_.mountCallbackRef()) {
owner_.mountCallbackRef()(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_.getSocket() || owner_.getState() != 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_.mountCallbackRef()) owner_.mountCallbackRef()(0);
owner_.currentMountDisplayIdRef() = 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_.getSocket()->send(basicPkt);
taxiWindowOpen_ = false;
taxiActivatePending_ = true;
taxiActivateTimer_ = 0.0f;
taxiStartGrace_ = 2.0f;
if (!onTaxiFlight_) {
onTaxiFlight_ = true;
sanitizeMovementForTaxi();
applyTaxiMountForCurrentNode();
}
if (owner_.getSocket()) {
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
}
if (owner_.taxiPrecacheCallbackRef()) {
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_.taxiPrecacheCallbackRef()(previewPath);
}
}
if (owner_.taxiFlightStartCallbackRef()) {
owner_.taxiFlightStartCallbackRef()();
}
startClientTaxiPath(path);
}
// ============================================================
// Area Trigger Detection (moved from GameHandler)
// ============================================================
void MovementHandler::loadAreaTriggerDbc() {
if (owner_.areaTriggerDbcLoadedRef()) return;
owner_.areaTriggerDbcLoadedRef() = true;
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("AreaTrigger.dbc");
if (!dbc || !dbc->isLoaded()) {
LOG_WARNING("Failed to load AreaTrigger.dbc");
return;
}
owner_.areaTriggersRef().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_.areaTriggersRef().push_back(at);
}
LOG_WARNING("Loaded ", owner_.areaTriggersRef().size(), " area triggers from AreaTrigger.dbc");
}
void MovementHandler::checkAreaTriggers() {
if (!owner_.isInWorld()) return;
if (onTaxiFlight_ || taxiClientActive_) return;
loadAreaTriggerDbc();
if (owner_.areaTriggersRef().empty()) return;
const float px = movementInfo.x;
const float py = movementInfo.y;
const float pz = movementInfo.z;
// Sanity: if position is near map origin on Eastern Kingdoms (map 0),
// something has corrupted movementInfo — skip area trigger check to
// avoid firing Alterac/Hillsbrad triggers and causing a rogue teleport.
if (owner_.getCurrentMapId() == 0 && std::abs(px) < 500.0f && std::abs(py) < 500.0f) {
LOG_WARNING("checkAreaTriggers: position near map origin (", px, ", ", py, ", ", pz,
") on map 0 — skipping to avoid rogue teleport. onTransport=",
owner_.isOnTransport(), " transportGuid=0x", std::hex,
owner_.playerTransportGuidRef(), std::dec);
return;
}
// 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_.areaTriggerSuppressFirstRef();
if (suppressFirst) {
owner_.areaTriggerSuppressFirstRef() = false;
}
for (const auto& at : owner_.areaTriggersRef()) {
if (at.mapId != owner_.currentMapIdRef()) continue;
bool inside = false;
if (at.radius > 0.0f) {
// Sphere trigger — use actual DBC radius
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 <= at.radius * at.radius);
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
// Box trigger — use actual DBC dimensions
float effLength = at.boxLength;
float effWidth = at.boxWidth;
float effHeight = at.boxHeight;
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_.activeAreaTriggersRef().count(at.id) == 0) {
owner_.activeAreaTriggersRef().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 {
// Send CMSG_AREATRIGGER — the player is already inside the
// trigger radius so the server's distance check should pass.
// Do NOT spoof position to the trigger center; that tells the
// server we're somewhere we're not and can cause rogue teleports.
network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER));
pkt.writeUInt32(at.id);
owner_.getSocket()->send(pkt);
LOG_DEBUG("Fired CMSG_AREATRIGGER: id=", at.id);
}
}
} else {
// Player left the trigger — allow re-fire on re-entry
owner_.activeAreaTriggersRef().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_.transportAttachmentsRef()[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_.transportAttachmentsRef().erase(childGuid);
}
void MovementHandler::updateAttachedTransportChildren(float /*deltaTime*/) {
if (!owner_.getTransportManager() || owner_.transportAttachmentsRef().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_.transportAttachmentsRef()) {
auto entity = owner_.getEntityManager().getEntity(childGuid);
if (!entity) {
stale.push_back(childGuid);
continue;
}
ActiveTransport* transport = owner_.getTransportManager()->getTransport(attachment.transportGuid);
if (!transport) {
continue;
}
glm::vec3 composed = owner_.getTransportManager()->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_.creatureMoveCallbackRef()) {
owner_.creatureMoveCallbackRef()(childGuid, composed.x, composed.y, composed.z, 0);
}
} else if (attachment.type == ObjectType::GAMEOBJECT) {
if (owner_.gameObjectMoveCallbackRef()) {
owner_.gameObjectMoveCallbackRef()(childGuid, composed.x, composed.y, composed.z, composedOrientation);
}
}
}
for (uint64_t guid : stale) {
owner_.transportAttachmentsRef().erase(guid);
}
}
// ============================================================
// Follow target (moved from GameHandler)
// ============================================================
void MovementHandler::followTarget() {
if (owner_.getState() != WorldState::IN_WORLD) {
LOG_WARNING("Cannot follow: not in world");
return;
}
if (owner_.getTargetGuid() == 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_.followTargetGuidRef() = owner_.getTargetGuid();
// Initialize render-space position from entity's canonical coords
owner_.followRenderPosRef() = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
// Tell camera controller to start auto-following
if (owner_.autoFollowCallbackRef()) {
owner_.autoFollowCallbackRef()(&owner_.followRenderPosRef());
}
// 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_.getTargetGuid(), std::dec, ")");
owner_.fireAddonEvent("AUTOFOLLOW_BEGIN", {});
}
void MovementHandler::cancelFollow() {
if (owner_.followTargetGuidRef() == 0) {
return;
}
owner_.followTargetGuidRef() = 0;
if (owner_.autoFollowCallbackRef()) {
owner_.autoFollowCallbackRef()(nullptr);
}
owner_.addSystemChatMessage("You stop following.");
owner_.fireAddonEvent("AUTOFOLLOW_END", {});
}
} // namespace game
} // namespace wowee