#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 #include #include #include #include #include #include namespace wowee { namespace game { MovementHandler::MovementHandler(GameHandler& owner) : owner_(owner), movementInfo(owner.movementInfo) {} void MovementHandler::registerOpcodes(DispatchTable& table) { // Creature movement table[Opcode::SMSG_MONSTER_MOVE] = [this](network::Packet& packet) { handleMonsterMove(packet); }; table[Opcode::SMSG_COMPRESSED_MOVES] = [this](network::Packet& packet) { handleCompressedMoves(packet); }; table[Opcode::SMSG_MONSTER_MOVE_TRANSPORT] = [this](network::Packet& packet) { handleMonsterMoveTransport(packet); }; // Spline move: consume-only (no state change) for (auto op : { Opcode::SMSG_SPLINE_MOVE_FEATHER_FALL, Opcode::SMSG_SPLINE_MOVE_GRAVITY_DISABLE, Opcode::SMSG_SPLINE_MOVE_GRAVITY_ENABLE, Opcode::SMSG_SPLINE_MOVE_LAND_WALK, Opcode::SMSG_SPLINE_MOVE_NORMAL_FALL, Opcode::SMSG_SPLINE_MOVE_ROOT, Opcode::SMSG_SPLINE_MOVE_SET_HOVER }) { table[op] = [this](network::Packet& packet) { if (packet.getSize() - packet.getReadPos() >= 1) (void)packet.readPackedGuid(); }; } // Spline move: synth flags (each opcode produces different flags) { auto makeSynthHandler = [this](uint32_t synthFlags) { return [this, synthFlags](network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 1) return; uint64_t guid = packet.readPackedGuid(); if (guid == 0 || guid == owner_.playerGuid || !owner_.unitMoveFlagsCallback_) return; owner_.unitMoveFlagsCallback_(guid, synthFlags); }; }; table[Opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE] = makeSynthHandler(0x00000100u); table[Opcode::SMSG_SPLINE_MOVE_SET_RUN_MODE] = makeSynthHandler(0u); table[Opcode::SMSG_SPLINE_MOVE_SET_FLYING] = makeSynthHandler(0x01000000u | 0x00800000u); table[Opcode::SMSG_SPLINE_MOVE_START_SWIM] = makeSynthHandler(0x00200000u); table[Opcode::SMSG_SPLINE_MOVE_STOP_SWIM] = makeSynthHandler(0u); } // Spline speed: all opcodes share the same PackedGuid+float format, differing // only in which member receives the value. Factory avoids 8 copy-pasted lambdas. auto makeSplineSpeedHandler = [this](float MovementHandler::* member) { return [this, member](network::Packet& packet) { if (!packet.hasRemaining(5)) return; uint64_t guid = packet.readPackedGuid(); if (!packet.hasRemaining(4)) return; float speed = packet.readFloat(); if (guid == owner_.playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f) this->*member = speed; }; }; table[Opcode::SMSG_SPLINE_SET_RUN_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverRunSpeed_); table[Opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverRunBackSpeed_); table[Opcode::SMSG_SPLINE_SET_SWIM_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverSwimSpeed_); // Force speed changes table[Opcode::SMSG_FORCE_RUN_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceRunSpeedChange(packet); }; table[Opcode::SMSG_FORCE_MOVE_ROOT] = [this](network::Packet& packet) { handleForceMoveRootState(packet, true); }; table[Opcode::SMSG_FORCE_MOVE_UNROOT] = [this](network::Packet& packet) { handleForceMoveRootState(packet, false); }; table[Opcode::SMSG_FORCE_WALK_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "WALK_SPEED", Opcode::CMSG_FORCE_WALK_SPEED_CHANGE_ACK, &serverWalkSpeed_); }; table[Opcode::SMSG_FORCE_RUN_BACK_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "RUN_BACK_SPEED", Opcode::CMSG_FORCE_RUN_BACK_SPEED_CHANGE_ACK, &serverRunBackSpeed_); }; table[Opcode::SMSG_FORCE_SWIM_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "SWIM_SPEED", Opcode::CMSG_FORCE_SWIM_SPEED_CHANGE_ACK, &serverSwimSpeed_); }; table[Opcode::SMSG_FORCE_SWIM_BACK_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "SWIM_BACK_SPEED", Opcode::CMSG_FORCE_SWIM_BACK_SPEED_CHANGE_ACK, &serverSwimBackSpeed_); }; table[Opcode::SMSG_FORCE_FLIGHT_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "FLIGHT_SPEED", Opcode::CMSG_FORCE_FLIGHT_SPEED_CHANGE_ACK, &serverFlightSpeed_); }; table[Opcode::SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "FLIGHT_BACK_SPEED", Opcode::CMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE_ACK, &serverFlightBackSpeed_); }; table[Opcode::SMSG_FORCE_TURN_RATE_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "TURN_RATE", Opcode::CMSG_FORCE_TURN_RATE_CHANGE_ACK, &serverTurnRate_); }; table[Opcode::SMSG_FORCE_PITCH_RATE_CHANGE] = [this](network::Packet& packet) { handleForceSpeedChange(packet, "PITCH_RATE", Opcode::CMSG_FORCE_PITCH_RATE_CHANGE_ACK, &serverPitchRate_); }; // Movement flag toggles table[Opcode::SMSG_MOVE_SET_CAN_FLY] = [this](network::Packet& packet) { handleForceMoveFlagChange(packet, "SET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK, static_cast(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(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(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(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(MovementFlags::HOVER), true); }; table[Opcode::SMSG_MOVE_UNSET_HOVER] = [this](network::Packet& packet) { handleForceMoveFlagChange(packet, "UNSET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK, static_cast(MovementFlags::HOVER), false); }; table[Opcode::SMSG_MOVE_KNOCK_BACK] = [this](network::Packet& packet) { handleMoveKnockBack(packet); }; // Teleport for (auto op : { Opcode::MSG_MOVE_TELEPORT, Opcode::MSG_MOVE_TELEPORT_ACK }) { table[op] = [this](network::Packet& packet) { handleTeleportAck(packet); }; } table[Opcode::SMSG_NEW_WORLD] = [this](network::Packet& packet) { handleNewWorld(packet); }; // Taxi table[Opcode::SMSG_SHOWTAXINODES] = [this](network::Packet& packet) { handleShowTaxiNodes(packet); }; table[Opcode::SMSG_ACTIVATETAXIREPLY] = [this](network::Packet& packet) { handleActivateTaxiReply(packet); }; // MSG_MOVE_* relay (other player movement) for (auto op : { Opcode::MSG_MOVE_START_FORWARD, Opcode::MSG_MOVE_START_BACKWARD, Opcode::MSG_MOVE_STOP, Opcode::MSG_MOVE_START_STRAFE_LEFT, Opcode::MSG_MOVE_START_STRAFE_RIGHT, Opcode::MSG_MOVE_STOP_STRAFE, Opcode::MSG_MOVE_JUMP, Opcode::MSG_MOVE_START_TURN_LEFT, Opcode::MSG_MOVE_START_TURN_RIGHT, Opcode::MSG_MOVE_STOP_TURN, Opcode::MSG_MOVE_SET_FACING, Opcode::MSG_MOVE_FALL_LAND, Opcode::MSG_MOVE_HEARTBEAT, Opcode::MSG_MOVE_START_SWIM, Opcode::MSG_MOVE_STOP_SWIM, Opcode::MSG_MOVE_SET_WALK_MODE, Opcode::MSG_MOVE_SET_RUN_MODE, Opcode::MSG_MOVE_START_PITCH_UP, Opcode::MSG_MOVE_START_PITCH_DOWN, Opcode::MSG_MOVE_STOP_PITCH, Opcode::MSG_MOVE_START_ASCEND, Opcode::MSG_MOVE_STOP_ASCEND, Opcode::MSG_MOVE_START_DESCEND, Opcode::MSG_MOVE_SET_PITCH, Opcode::MSG_MOVE_GRAVITY_CHNG, Opcode::MSG_MOVE_UPDATE_CAN_FLY, Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY, Opcode::MSG_MOVE_ROOT, Opcode::MSG_MOVE_UNROOT }) { table[op] = [this](network::Packet& packet) { if (owner_.getState() == WorldState::IN_WORLD) handleOtherPlayerMovement(packet); }; } // MSG_MOVE_SET_*_SPEED relay for (auto op : { Opcode::MSG_MOVE_SET_RUN_SPEED, Opcode::MSG_MOVE_SET_RUN_BACK_SPEED, Opcode::MSG_MOVE_SET_WALK_SPEED, Opcode::MSG_MOVE_SET_SWIM_SPEED, Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED, Opcode::MSG_MOVE_SET_FLIGHT_SPEED, Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED }) { table[op] = [this](network::Packet& packet) { if (owner_.getState() == WorldState::IN_WORLD) handleMoveSetSpeed(packet); }; } // ---- Client control & spline speed/flag changes ---- // Client control update table[Opcode::SMSG_CLIENT_CONTROL_UPDATE] = [this](network::Packet& packet) { handleClientControlUpdate(packet); }; // Spline move flag changes for other units (unroot/unset_hover/water_walk) for (auto op : {Opcode::SMSG_SPLINE_MOVE_UNROOT, Opcode::SMSG_SPLINE_MOVE_UNSET_HOVER, Opcode::SMSG_SPLINE_MOVE_WATER_WALK}) { table[op] = [this](network::Packet& packet) { // Minimal parse: PackedGuid only — no animation-relevant state change. if (packet.hasRemaining(1)) { (void)packet.readPackedGuid(); } }; } table[Opcode::SMSG_SPLINE_MOVE_UNSET_FLYING] = [this](network::Packet& packet) { // PackedGuid + synthesised move-flags=0 → clears flying animation. if (!packet.hasRemaining(1)) return; uint64_t guid = packet.readPackedGuid(); if (guid == 0 || guid == owner_.playerGuid || !owner_.unitMoveFlagsCallback_) return; owner_.unitMoveFlagsCallback_(guid, 0u); // clear flying/CAN_FLY }; // Remaining spline speed opcodes — same factory as above. table[Opcode::SMSG_SPLINE_SET_FLIGHT_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverFlightSpeed_); table[Opcode::SMSG_SPLINE_SET_FLIGHT_BACK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverFlightBackSpeed_); table[Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverSwimBackSpeed_); table[Opcode::SMSG_SPLINE_SET_WALK_SPEED] = makeSplineSpeedHandler(&MovementHandler::serverWalkSpeed_); table[Opcode::SMSG_SPLINE_SET_TURN_RATE] = makeSplineSpeedHandler(&MovementHandler::serverTurnRate_); // Pitch rate not stored locally — consume packet to keep stream aligned. table[Opcode::SMSG_SPLINE_SET_PITCH_RATE] = [](network::Packet& packet) { packet.skipAll(); }; // ---- Player movement flag changes (server-pushed) ---- table[Opcode::SMSG_MOVE_GRAVITY_DISABLE] = [this](network::Packet& packet) { handleForceMoveFlagChange(packet, "GRAVITY_DISABLE", Opcode::CMSG_MOVE_GRAVITY_DISABLE_ACK, static_cast(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(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(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(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(MovementFlags::FLYING), true); }; table[Opcode::SMSG_MOVE_UNSET_FLIGHT] = [this](network::Packet& packet) { handleForceMoveFlagChange(packet, "UNSET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK, static_cast(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(b) << (i * 8)); } } bool allowMovement = (packet.readUInt8() != 0); if (controlGuid == 0 || controlGuid == owner_.playerGuid) { bool changed = (serverMovementAllowed_ != allowMovement); serverMovementAllowed_ = allowMovement; if (changed && !allowMovement) { // Force-stop local movement immediately when server revokes control. movementInfo.flags &= ~(static_cast(MovementFlags::FORWARD) | static_cast(MovementFlags::BACKWARD) | static_cast(MovementFlags::STRAFE_LEFT) | static_cast(MovementFlags::STRAFE_RIGHT) | static_cast(MovementFlags::TURN_LEFT) | static_cast(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( std::chrono::duration_cast(now - movementClockStart_).count()) + 1ULL; if (elapsed > std::numeric_limits::max()) { movementClockStart_ = now; elapsed = 1ULL; } uint32_t candidate = static_cast(elapsed); if (candidate <= lastMovementTimestampMs_) { candidate = lastMovementTimestampMs_ + 1U; if (candidate == 0) { movementClockStart_ = now; candidate = 1U; } } lastMovementTimestampMs_ = candidate; return candidate; } // ============================================================ // sendMovement // ============================================================ void MovementHandler::sendMovement(Opcode opcode) { if (owner_.state != WorldState::IN_WORLD) { LOG_WARNING("Cannot send movement in state: ", (int)owner_.state); return; } // Block manual movement while taxi is active/mounted, but always allow // stop/heartbeat opcodes so stuck states can be recovered. bool taxiAllowed = (opcode == Opcode::MSG_MOVE_HEARTBEAT) || (opcode == Opcode::MSG_MOVE_STOP) || (opcode == Opcode::MSG_MOVE_STOP_STRAFE) || (opcode == Opcode::MSG_MOVE_STOP_TURN) || (opcode == Opcode::MSG_MOVE_STOP_SWIM); if (!serverMovementAllowed_ && !taxiAllowed) return; if ((onTaxiFlight_ || taxiMountActive_) && !taxiAllowed) return; if (owner_.resurrectPending_ && !taxiAllowed) return; // Always send a strictly increasing non-zero client movement clock value. const uint32_t movementTime = nextMovementTimestampMs(); movementInfo.time = movementTime; if (opcode == Opcode::MSG_MOVE_SET_FACING && (isClassicLikeExpansion() || isActiveExpansion("tbc"))) { const float facingDelta = core::coords::normalizeAngleRad( movementInfo.orientation - lastFacingSentOrientation_); const uint32_t sinceLastFacingMs = lastFacingSendTimeMs_ != 0 && movementTime >= lastFacingSendTimeMs_ ? (movementTime - lastFacingSendTimeMs_) : std::numeric_limits::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(MovementFlags::FORWARD) | static_cast(MovementFlags::BACKWARD) | static_cast(MovementFlags::STRAFE_LEFT) | static_cast(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(MovementFlags::FORWARD); break; case Opcode::MSG_MOVE_START_BACKWARD: movementInfo.flags |= static_cast(MovementFlags::BACKWARD); break; case Opcode::MSG_MOVE_STOP: movementInfo.flags &= ~(static_cast(MovementFlags::FORWARD) | static_cast(MovementFlags::BACKWARD)); break; case Opcode::MSG_MOVE_START_STRAFE_LEFT: movementInfo.flags |= static_cast(MovementFlags::STRAFE_LEFT); break; case Opcode::MSG_MOVE_START_STRAFE_RIGHT: movementInfo.flags |= static_cast(MovementFlags::STRAFE_RIGHT); break; case Opcode::MSG_MOVE_STOP_STRAFE: movementInfo.flags &= ~(static_cast(MovementFlags::STRAFE_LEFT) | static_cast(MovementFlags::STRAFE_RIGHT)); break; case Opcode::MSG_MOVE_JUMP: movementInfo.flags |= static_cast(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(MovementFlags::FORWARD) | static_cast(MovementFlags::BACKWARD) | static_cast(MovementFlags::STRAFE_LEFT) | static_cast(MovementFlags::STRAFE_RIGHT); const bool movingHoriz = (movementInfo.flags & horizFlags) != 0; if (movingHoriz) { const bool isWalking = (movementInfo.flags & static_cast(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(MovementFlags::TURN_LEFT); break; case Opcode::MSG_MOVE_START_TURN_RIGHT: movementInfo.flags |= static_cast(MovementFlags::TURN_RIGHT); break; case Opcode::MSG_MOVE_STOP_TURN: movementInfo.flags &= ~(static_cast(MovementFlags::TURN_LEFT) | static_cast(MovementFlags::TURN_RIGHT)); break; case Opcode::MSG_MOVE_FALL_LAND: movementInfo.flags &= ~static_cast(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(MovementFlags::ASCENDING); movementInfo.flags &= ~static_cast(MovementFlags::DESCENDING); break; case Opcode::MSG_MOVE_STOP_ASCEND: movementInfo.flags &= ~static_cast(MovementFlags::ASCENDING); movementInfo.flags &= ~static_cast(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(MovementFlags::ASCENDING); movementInfo.flags |= static_cast(MovementFlags::DESCENDING); break; default: break; } // Fire PLAYER_STARTED/STOPPED_MOVING on movement state transitions { const bool isMoving = (movementInfo.flags & kMoveMask) != 0; if (isMoving && !wasMoving && owner_.addonEventCallback_) owner_.addonEventCallback_("PLAYER_STARTED_MOVING", {}); else if (!isMoving && wasMoving && owner_.addonEventCallback_) owner_.addonEventCallback_("PLAYER_STOPPED_MOVING", {}); } if (opcode == Opcode::MSG_MOVE_SET_FACING) { lastFacingSendTimeMs_ = movementInfo.time; lastFacingSentOrientation_ = movementInfo.orientation; } // Keep fallTime current if (isFalling_ && movementInfo.hasFlag(MovementFlags::FALLING)) { uint32_t elapsed = (movementInfo.time >= fallStartMs_) ? (movementInfo.time - fallStartMs_) : 0u; movementInfo.fallTime = elapsed; } else if (!movementInfo.hasFlag(MovementFlags::FALLING)) { if (isFalling_) { isFalling_ = false; fallStartMs_ = 0; } movementInfo.fallTime = 0; } if (onTaxiFlight_ || taxiMountActive_ || taxiActivatePending_ || taxiClientActive_) { sanitizeMovementForTaxi(); } bool includeTransportInWire = owner_.isOnTransport(); if (includeTransportInWire && owner_.transportManager_) { if (auto* tr = owner_.transportManager_->getTransport(owner_.playerTransportGuid_); tr && tr->isM2) { includeTransportInWire = false; } } // Add transport data if player is on a server-recognized transport if (includeTransportInWire) { if (owner_.transportManager_) { glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition(owner_.playerTransportGuid_, owner_.playerTransportOffset_); movementInfo.x = composed.x; movementInfo.y = composed.y; movementInfo.z = composed.z; } movementInfo.flags |= static_cast(MovementFlags::ONTRANSPORT); movementInfo.transportGuid = owner_.playerTransportGuid_; movementInfo.transportX = owner_.playerTransportOffset_.x; movementInfo.transportY = owner_.playerTransportOffset_.y; movementInfo.transportZ = owner_.playerTransportOffset_.z; movementInfo.transportTime = movementInfo.time; movementInfo.transportSeat = -1; movementInfo.transportTime2 = movementInfo.time; float transportYawCanonical = 0.0f; if (owner_.transportManager_) { if (auto* tr = owner_.transportManager_->getTransport(owner_.playerTransportGuid_); tr) { if (tr->hasServerYaw) { transportYawCanonical = tr->serverYaw; } else { transportYawCanonical = glm::eulerAngles(tr->rotation).z; } } } movementInfo.transportO = core::coords::normalizeAngleRad(movementInfo.orientation - transportYawCanonical); } else { movementInfo.flags &= ~static_cast(MovementFlags::ONTRANSPORT); movementInfo.transportGuid = 0; movementInfo.transportSeat = -1; } if (opcode == Opcode::MSG_MOVE_HEARTBEAT && isClassicLikeExpansion()) { const uint32_t locomotionFlags = static_cast(MovementFlags::FORWARD) | static_cast(MovementFlags::BACKWARD) | static_cast(MovementFlags::STRAFE_LEFT) | static_cast(MovementFlags::STRAFE_RIGHT) | static_cast(MovementFlags::TURN_LEFT) | static_cast(MovementFlags::TURN_RIGHT) | static_cast(MovementFlags::ASCENDING) | static_cast(MovementFlags::FALLING) | static_cast(MovementFlags::FALLINGFAR) | static_cast(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::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::max(); if (sinceLastNonHeartbeatMoveMs < 350U) { timeSinceLastMoveHeartbeat_ = 0.0f; return; } } LOG_DEBUG("Sending movement packet: opcode=0x", std::hex, wireOpcode(opcode), std::dec, (includeTransportInWire ? " ONTRANSPORT" : "")); // Convert canonical → server coordinates for the wire MovementInfo wireInfo = movementInfo; glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wireInfo.x, wireInfo.y, wireInfo.z)); wireInfo.x = serverPos.x; wireInfo.y = serverPos.y; wireInfo.z = serverPos.z; // Periodic position audit — DEBUG to avoid flooding production logs. if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 30 == 0) { LOG_DEBUG("HEARTBEAT pos canonical=(", movementInfo.x, ",", movementInfo.y, ",", movementInfo.z, ") wire=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z, ")"); } wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation); if (includeTransportInWire) { glm::vec3 serverTransportPos = core::coords::canonicalToServer( glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ)); wireInfo.transportX = serverTransportPos.x; wireInfo.transportY = serverTransportPos.y; wireInfo.transportZ = serverTransportPos.z; wireInfo.transportO = core::coords::normalizeAngleRad(-wireInfo.transportO); } // Build and send movement packet (expansion-specific format) auto packet = owner_.packetParsers_ ? owner_.packetParsers_->buildMovementPacket(opcode, wireInfo, owner_.playerGuid) : MovementPacket::build(opcode, wireInfo, owner_.playerGuid); owner_.socket->send(packet); if (opcode == Opcode::MSG_MOVE_HEARTBEAT) { lastHeartbeatSendTimeMs_ = movementInfo.time; lastHeartbeatX_ = movementInfo.x; lastHeartbeatY_ = movementInfo.y; lastHeartbeatZ_ = movementInfo.z; lastHeartbeatFlags_ = movementInfo.flags; lastHeartbeatTransportGuid_ = movementInfo.transportGuid; } else { lastNonHeartbeatMoveSendTimeMs_ = movementInfo.time; } } // ============================================================ // sanitizeMovementForTaxi // ============================================================ void MovementHandler::sanitizeMovementForTaxi() { constexpr uint32_t kClearTaxiFlags = static_cast(MovementFlags::FORWARD) | static_cast(MovementFlags::BACKWARD) | static_cast(MovementFlags::STRAFE_LEFT) | static_cast(MovementFlags::STRAFE_RIGHT) | static_cast(MovementFlags::TURN_LEFT) | static_cast(MovementFlags::TURN_RIGHT) | static_cast(MovementFlags::PITCH_UP) | static_cast(MovementFlags::PITCH_DOWN) | static_cast(MovementFlags::FALLING) | static_cast(MovementFlags::FALLINGFAR) | static_cast(MovementFlags::SWIMMING); movementInfo.flags &= ~kClearTaxiFlags; movementInfo.fallTime = 0; movementInfo.jumpVelocity = 0.0f; movementInfo.jumpSinAngle = 0.0f; movementInfo.jumpCosAngle = 0.0f; movementInfo.jumpXYSpeed = 0.0f; movementInfo.pitch = 0.0f; } // ============================================================ // forceClearTaxiAndMovementState // ============================================================ void MovementHandler::forceClearTaxiAndMovementState() { taxiActivatePending_ = false; taxiActivateTimer_ = 0.0f; taxiClientActive_ = false; taxiClientPath_.clear(); taxiRecoverPending_ = false; taxiStartGrace_ = 0.0f; onTaxiFlight_ = false; if (taxiMountActive_ && owner_.mountCallback_) { owner_.mountCallback_(0); } taxiMountActive_ = false; taxiMountDisplayId_ = 0; owner_.currentMountDisplayId_ = 0; owner_.vehicleId_ = 0; owner_.resurrectPending_ = false; owner_.resurrectRequestPending_ = false; owner_.selfResAvailable_ = false; owner_.playerDead_ = false; owner_.releasedSpirit_ = false; owner_.corpseGuid_ = 0; owner_.corpseReclaimAvailableMs_ = 0; owner_.repopPending_ = false; owner_.pendingSpiritHealerGuid_ = 0; owner_.resurrectCasterGuid_ = 0; movementInfo.flags = 0; movementInfo.flags2 = 0; movementInfo.transportGuid = 0; owner_.clearPlayerTransport(); if (owner_.socket && owner_.state == WorldState::IN_WORLD) { sendMovement(Opcode::MSG_MOVE_STOP); sendMovement(Opcode::MSG_MOVE_STOP_STRAFE); sendMovement(Opcode::MSG_MOVE_STOP_TURN); sendMovement(Opcode::MSG_MOVE_STOP_SWIM); sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } LOG_INFO("Force-cleared taxi/movement state"); } // ============================================================ // setPosition / setOrientation // ============================================================ void MovementHandler::setPosition(float x, float y, float z) { movementInfo.x = x; movementInfo.y = y; movementInfo.z = z; } void MovementHandler::setOrientation(float orientation) { movementInfo.orientation = orientation; } // ============================================================ // dismount // ============================================================ void MovementHandler::dismount() { if (!owner_.socket) return; uint32_t savedMountAura = owner_.mountAuraSpellId_; if (owner_.currentMountDisplayId_ != 0 || taxiMountActive_) { if (owner_.mountCallback_) { owner_.mountCallback_(0); } owner_.currentMountDisplayId_ = 0; taxiMountActive_ = false; taxiMountDisplayId_ = 0; owner_.mountAuraSpellId_ = 0; LOG_INFO("Dismount: cleared local mount state"); } uint16_t cancelMountWire = wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA); if (cancelMountWire != 0xFFFF) { network::Packet pkt(cancelMountWire); owner_.socket->send(pkt); LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA"); } else if (savedMountAura != 0) { auto pkt = CancelAuraPacket::build(savedMountAura); owner_.socket->send(pkt); LOG_INFO("Sent CMSG_CANCEL_AURA (mount spell ", savedMountAura, ") — Classic fallback"); } else { for (const auto& a : owner_.getPlayerAuras()) { if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == owner_.playerGuid) { auto pkt = CancelAuraPacket::build(a.spellId); owner_.socket->send(pkt); LOG_INFO("Sent CMSG_CANCEL_AURA (spell ", a.spellId, ") — brute force dismount"); } } } } // ============================================================ // Force Speed / Root / Flag Change Handlers // ============================================================ void MovementHandler::handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage) { const bool fscTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); uint64_t guid = fscTbcLike ? packet.readUInt64() : packet.readPackedGuid(); uint32_t counter = packet.readUInt32(); size_t remaining = packet.getSize() - packet.getReadPos(); if (remaining >= 8) { packet.readUInt32(); } else if (remaining >= 5) { packet.readUInt8(); } float newSpeed = packet.readFloat(); LOG_INFO("SMSG_FORCE_", name, "_CHANGE: guid=0x", std::hex, guid, std::dec, " counter=", counter, " speed=", newSpeed); if (guid != owner_.playerGuid) return; if (owner_.socket) { network::Packet ack(wireOpcode(ackOpcode)); const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle"); if (legacyGuidAck) { ack.writeUInt64(owner_.playerGuid); } else { ack.writePackedGuid(owner_.playerGuid); } ack.writeUInt32(counter); MovementInfo wire = movementInfo; wire.time = nextMovementTimestampMs(); if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { wire.transportTime = wire.time; wire.transportTime2 = wire.time; } glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z)); wire.x = serverPos.x; wire.y = serverPos.y; wire.z = serverPos.z; if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { glm::vec3 serverTransport = core::coords::canonicalToServer(glm::vec3(wire.transportX, wire.transportY, wire.transportZ)); wire.transportX = serverTransport.x; wire.transportY = serverTransport.y; wire.transportZ = serverTransport.z; } if (owner_.packetParsers_) { owner_.packetParsers_->writeMovementPayload(ack, wire); } else { MovementPacket::writeMovementPayload(ack, wire); } ack.writeFloat(newSpeed); owner_.socket->send(ack); } if (std::isnan(newSpeed) || newSpeed < 0.1f || newSpeed > 100.0f) { LOG_WARNING("Ignoring invalid ", name, " speed: ", newSpeed); return; } if (speedStorage) *speedStorage = newSpeed; } void MovementHandler::handleForceRunSpeedChange(network::Packet& packet) { handleForceSpeedChange(packet, "RUN_SPEED", Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, &serverRunSpeed_); if (!onTaxiFlight_ && !taxiMountActive_ && owner_.currentMountDisplayId_ != 0 && serverRunSpeed_ <= 8.5f) { LOG_INFO("Auto-clearing mount from speed change: speed=", serverRunSpeed_, " displayId=", owner_.currentMountDisplayId_); owner_.currentMountDisplayId_ = 0; if (owner_.mountCallback_) { owner_.mountCallback_(0); } } } void MovementHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) { const bool rootTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); if (packet.getSize() - packet.getReadPos() < (rootTbc ? 8u : 2u)) return; uint64_t guid = rootTbc ? packet.readUInt64() : packet.readPackedGuid(); if (packet.getSize() - packet.getReadPos() < 4) return; uint32_t counter = packet.readUInt32(); LOG_INFO(rooted ? "SMSG_FORCE_MOVE_ROOT" : "SMSG_FORCE_MOVE_UNROOT", ": guid=0x", std::hex, guid, std::dec, " counter=", counter); if (guid != owner_.playerGuid) return; if (rooted) { movementInfo.flags |= static_cast(MovementFlags::ROOT); } else { movementInfo.flags &= ~static_cast(MovementFlags::ROOT); } if (!owner_.socket) return; uint16_t ackWire = wireOpcode(rooted ? Opcode::CMSG_FORCE_MOVE_ROOT_ACK : Opcode::CMSG_FORCE_MOVE_UNROOT_ACK); if (ackWire == 0xFFFF) return; network::Packet ack(ackWire); const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle"); if (legacyGuidAck) { ack.writeUInt64(owner_.playerGuid); } else { ack.writePackedGuid(owner_.playerGuid); } ack.writeUInt32(counter); MovementInfo wire = movementInfo; wire.time = nextMovementTimestampMs(); if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { wire.transportTime = wire.time; wire.transportTime2 = wire.time; } glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z)); wire.x = serverPos.x; wire.y = serverPos.y; wire.z = serverPos.z; if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { glm::vec3 serverTransport = core::coords::canonicalToServer(glm::vec3(wire.transportX, wire.transportY, wire.transportZ)); wire.transportX = serverTransport.x; wire.transportY = serverTransport.y; wire.transportZ = serverTransport.z; } if (owner_.packetParsers_) owner_.packetParsers_->writeMovementPayload(ack, wire); else MovementPacket::writeMovementPayload(ack, wire); owner_.socket->send(ack); } void MovementHandler::handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set) { const bool fmfTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); if (packet.getSize() - packet.getReadPos() < (fmfTbcLike ? 8u : 2u)) return; uint64_t guid = fmfTbcLike ? packet.readUInt64() : packet.readPackedGuid(); if (packet.getSize() - packet.getReadPos() < 4) return; uint32_t counter = packet.readUInt32(); LOG_INFO("SMSG_FORCE_", name, ": guid=0x", std::hex, guid, std::dec, " counter=", counter); if (guid != owner_.playerGuid) return; if (flag != 0) { if (set) { movementInfo.flags |= flag; } else { movementInfo.flags &= ~flag; } } if (!owner_.socket) return; uint16_t ackWire = wireOpcode(ackOpcode); if (ackWire == 0xFFFF) return; network::Packet ack(ackWire); const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle"); if (legacyGuidAck) { ack.writeUInt64(owner_.playerGuid); } else { ack.writePackedGuid(owner_.playerGuid); } ack.writeUInt32(counter); MovementInfo wire = movementInfo; wire.time = nextMovementTimestampMs(); if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { wire.transportTime = wire.time; wire.transportTime2 = wire.time; } glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z)); wire.x = serverPos.x; wire.y = serverPos.y; wire.z = serverPos.z; if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { glm::vec3 serverTransport = core::coords::canonicalToServer(glm::vec3(wire.transportX, wire.transportY, wire.transportZ)); wire.transportX = serverTransport.x; wire.transportY = serverTransport.y; wire.transportZ = serverTransport.z; } if (owner_.packetParsers_) owner_.packetParsers_->writeMovementPayload(ack, wire); else MovementPacket::writeMovementPayload(ack, wire); owner_.socket->send(ack); } void MovementHandler::handleMoveSetCollisionHeight(network::Packet& packet) { const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc"); if (packet.getSize() - packet.getReadPos() < (legacyGuid ? 8u : 2u)) return; uint64_t guid = legacyGuid ? packet.readUInt64() : packet.readPackedGuid(); if (packet.getSize() - packet.getReadPos() < 8) return; uint32_t counter = packet.readUInt32(); float height = packet.readFloat(); LOG_INFO("SMSG_MOVE_SET_COLLISION_HGT: guid=0x", std::hex, guid, std::dec, " counter=", counter, " height=", height); if (guid != owner_.playerGuid) return; if (!owner_.socket) return; uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_SET_COLLISION_HGT_ACK); if (ackWire == 0xFFFF) return; network::Packet ack(ackWire); const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle"); if (legacyGuidAck) { ack.writeUInt64(owner_.playerGuid); } else { ack.writePackedGuid(owner_.playerGuid); } ack.writeUInt32(counter); MovementInfo wire = movementInfo; wire.time = nextMovementTimestampMs(); glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z)); wire.x = serverPos.x; wire.y = serverPos.y; wire.z = serverPos.z; if (owner_.packetParsers_) owner_.packetParsers_->writeMovementPayload(ack, wire); else MovementPacket::writeMovementPayload(ack, wire); ack.writeFloat(height); owner_.socket->send(ack); } void MovementHandler::handleMoveKnockBack(network::Packet& packet) { const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); if (packet.getSize() - packet.getReadPos() < (mkbTbc ? 8u : 2u)) return; uint64_t guid = mkbTbc ? packet.readUInt64() : packet.readPackedGuid(); if (packet.getSize() - packet.getReadPos() < 20) return; uint32_t counter = packet.readUInt32(); float vcos = packet.readFloat(); float vsin = packet.readFloat(); float hspeed = packet.readFloat(); float vspeed = packet.readFloat(); LOG_INFO("SMSG_MOVE_KNOCK_BACK: guid=0x", std::hex, guid, std::dec, " counter=", counter, " vcos=", vcos, " vsin=", vsin, " hspeed=", hspeed, " vspeed=", vspeed); if (guid != owner_.playerGuid) return; if (owner_.knockBackCallback_) { owner_.knockBackCallback_(vcos, vsin, hspeed, vspeed); } if (!owner_.socket) return; uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK); if (ackWire == 0xFFFF) return; network::Packet ack(ackWire); const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle"); if (legacyGuidAck) { ack.writeUInt64(owner_.playerGuid); } else { ack.writePackedGuid(owner_.playerGuid); } ack.writeUInt32(counter); MovementInfo wire = movementInfo; wire.time = nextMovementTimestampMs(); if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { wire.transportTime = wire.time; wire.transportTime2 = wire.time; } glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wire.x, wire.y, wire.z)); wire.x = serverPos.x; wire.y = serverPos.y; wire.z = serverPos.z; if (wire.hasFlag(MovementFlags::ONTRANSPORT)) { glm::vec3 serverTransport = core::coords::canonicalToServer(glm::vec3(wire.transportX, wire.transportY, wire.transportZ)); wire.transportX = serverTransport.x; wire.transportY = serverTransport.y; wire.transportZ = serverTransport.z; } if (owner_.packetParsers_) owner_.packetParsers_->writeMovementPayload(ack, wire); else MovementPacket::writeMovementPayload(ack, wire); owner_.socket->send(ack); } // ============================================================ // Other Player / Creature Movement Handlers // ============================================================ void MovementHandler::handleMoveSetSpeed(network::Packet& packet) { const bool useFull = isClassicLikeExpansion() || isActiveExpansion("tbc"); uint64_t moverGuid = useFull ? packet.readUInt64() : packet.readPackedGuid(); const size_t remaining = packet.getSize() - packet.getReadPos(); if (remaining < 4) return; if (remaining > 4) { packet.setReadPos(packet.getSize() - 4); } float speed = packet.readFloat(); if (!std::isfinite(speed) || speed <= 0.01f || speed > 200.0f) return; if (moverGuid != owner_.playerGuid) return; const uint16_t wireOp = packet.getOpcode(); if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_RUN_SPEED)) serverRunSpeed_ = speed; else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_RUN_BACK_SPEED)) serverRunBackSpeed_ = speed; else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_WALK_SPEED)) serverWalkSpeed_ = speed; else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_SWIM_SPEED)) serverSwimSpeed_ = speed; else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED)) serverSwimBackSpeed_ = speed; else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_FLIGHT_SPEED)) serverFlightSpeed_ = speed; else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED))serverFlightBackSpeed_= speed; } void MovementHandler::handleOtherPlayerMovement(network::Packet& packet) { const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); uint64_t moverGuid = otherMoveTbc ? packet.readUInt64() : packet.readPackedGuid(); if (moverGuid == owner_.playerGuid || moverGuid == 0) { return; } MovementInfo info = {}; info.flags = packet.readUInt32(); uint8_t flags2Size = owner_.packetParsers_ ? owner_.packetParsers_->movementFlags2Size() : 2; if (flags2Size == 2) info.flags2 = packet.readUInt16(); else if (flags2Size == 1) info.flags2 = packet.readUInt8(); info.time = packet.readUInt32(); info.x = packet.readFloat(); info.y = packet.readFloat(); info.z = packet.readFloat(); info.orientation = packet.readFloat(); const uint32_t wireTransportFlag = owner_.packetParsers_ ? owner_.packetParsers_->wireOnTransportFlag() : 0x00000200; const bool onTransport = (info.flags & wireTransportFlag) != 0; uint64_t transportGuid = 0; float tLocalX = 0, tLocalY = 0, tLocalZ = 0, tLocalO = 0; if (onTransport) { transportGuid = packet.readPackedGuid(); tLocalX = packet.readFloat(); tLocalY = packet.readFloat(); tLocalZ = packet.readFloat(); tLocalO = packet.readFloat(); if (flags2Size >= 1) { /*uint32_t transportTime =*/ packet.readUInt32(); } if (flags2Size >= 2) { /*int8_t transportSeat =*/ packet.readUInt8(); if (info.flags2 & 0x0200) { /*uint32_t transportTime2 =*/ packet.readUInt32(); } } } auto entity = owner_.getEntityManager().getEntity(moverGuid); if (!entity) { return; } glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(info.x, info.y, info.z)); float canYaw = core::coords::serverToCanonicalYaw(info.orientation); if (onTransport && transportGuid != 0 && owner_.transportManager_) { glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(tLocalX, tLocalY, tLocalZ)); owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, true, core::coords::serverToCanonicalYaw(tLocalO)); glm::vec3 worldPos = owner_.transportManager_->getPlayerWorldPosition(transportGuid, localCanonical); canonical = worldPos; } else if (!onTransport) { owner_.clearTransportAttachment(moverGuid); } uint32_t durationMs = 120; auto itPrev = otherPlayerMoveTimeMs_.find(moverGuid); if (itPrev != otherPlayerMoveTimeMs_.end()) { uint32_t rawDt = info.time - itPrev->second; if (rawDt >= 20 && rawDt <= 2000) { float fDt = static_cast(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(clamped); } } otherPlayerMoveTimeMs_[moverGuid] = info.time; const uint16_t wireOp = packet.getOpcode(); const bool isStopOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP)) || (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_STRAFE)) || (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_TURN)) || (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_SWIM)) || (wireOp == wireOpcode(Opcode::MSG_MOVE_FALL_LAND)); const bool isJumpOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_JUMP)); const float entityDuration = isStopOpcode ? 0.0f : (durationMs / 1000.0f); entity->startMoveTo(canonical.x, canonical.y, canonical.z, canYaw, entityDuration); if (owner_.creatureMoveCallback_) { const uint32_t notifyDuration = isStopOpcode ? 0u : durationMs; owner_.creatureMoveCallback_(moverGuid, canonical.x, canonical.y, canonical.z, notifyDuration); } if (owner_.unitAnimHintCallback_ && isJumpOpcode) { owner_.unitAnimHintCallback_(moverGuid, 38u); } if (owner_.unitMoveFlagsCallback_) { owner_.unitMoveFlagsCallback_(moverGuid, info.flags); } } void MovementHandler::handleCompressedMoves(network::Packet& packet) { std::vector decompressedStorage; const std::vector* 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(rawData[0]) | (static_cast(rawData[1]) << 8) | (static_cast(rawData[2]) << 16) | (static_cast(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 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 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 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(subSize) : (subSize >= 2 ? static_cast(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(data[pos + 1]) | (static_cast(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 unhandledSeen; for (const auto& entry : decoded.packets) { network::Packet subPacket(entry.opcode, entry.payload); if (entry.opcode == monsterMoveWire) { handleMonsterMove(subPacket); } else if (entry.opcode == monsterMoveTransportWire) { handleMonsterMoveTransport(subPacket); } else if (owner_.state == WorldState::IN_WORLD && std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), entry.opcode) != kMoveOpcodes.end()) { handleOtherPlayerMovement(subPacket); } else { if (unhandledSeen.insert(entry.opcode).second) { LOG_INFO("SMSG_COMPRESSED_MOVES: unhandled sub-opcode 0x", std::hex, entry.opcode, std::dec, " payloadLen=", entry.payload.size()); } } } } // ============================================================ // Monster Move Handlers // ============================================================ void MovementHandler::handleMonsterMove(network::Packet& packet) { if (isActiveExpansion("classic") || isActiveExpansion("turtle")) { constexpr uint32_t kMaxMonsterMovesPerTick = 256; ++monsterMovePacketsThisTick_; if (monsterMovePacketsThisTick_ > kMaxMonsterMovesPerTick) { ++monsterMovePacketsDroppedThisTick_; if (monsterMovePacketsDroppedThisTick_ <= 3 || (monsterMovePacketsDroppedThisTick_ % 100) == 0) { LOG_WARNING("SMSG_MONSTER_MOVE: per-tick cap exceeded, dropping packet", " (processed=", monsterMovePacketsThisTick_, " dropped=", monsterMovePacketsDroppedThisTick_, ")"); } return; } } MonsterMoveData data; auto logMonsterMoveParseFailure = [&](const std::string& msg) { static uint32_t failCount = 0; ++failCount; if (failCount <= 10 || (failCount % 100) == 0) { LOG_WARNING(msg, " (occurrence=", failCount, ")"); } }; auto logWrappedUncompressedFallbackUsed = [&]() { static uint32_t wrappedUncompressedFallbackCount = 0; ++wrappedUncompressedFallbackCount; if (wrappedUncompressedFallbackCount <= 10 || (wrappedUncompressedFallbackCount % 100) == 0) { LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback", " (occurrence=", wrappedUncompressedFallbackCount, ")"); } }; auto stripWrappedSubpacket = [&](const std::vector& bytes, std::vector& stripped) -> bool { if (bytes.size() < 3) return false; uint8_t subSize = bytes[0]; if (subSize < 2) return false; size_t wrappedLen = static_cast(subSize) + 1; if (wrappedLen != bytes.size()) return false; size_t payloadLen = static_cast(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(rawData[0]) | (static_cast(rawData[1]) << 8) | (static_cast(rawData[2]) << 16) | (static_cast(rawData[3]) << 24); if (decompSize == 0 || decompSize > 65536) { LOG_WARNING("SMSG_MONSTER_MOVE: bad decompSize=", decompSize); return; } std::vector 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 stripped; bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped); bool parsed = false; if (hasWrappedForm) { network::Packet wrappedPacket(packet.getOpcode(), stripped); if (owner_.packetParsers_->parseMonsterMove(wrappedPacket, data)) { parsed = true; } } if (!parsed) { network::Packet decompPacket(packet.getOpcode(), decompressed); if (owner_.packetParsers_->parseMonsterMove(decompPacket, data)) { parsed = true; } } if (!parsed) { if (hasWrappedForm) { logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " + std::to_string(destLen) + " bytes, wrapped payload " + std::to_string(stripped.size()) + " bytes)"); } else { logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " + std::to_string(destLen) + " bytes)"); } return; } } else if (!owner_.packetParsers_->parseMonsterMove(packet, data)) { std::vector stripped; if (stripWrappedSubpacket(rawData, stripped)) { network::Packet wrappedPacket(packet.getOpcode(), stripped); if (owner_.packetParsers_->parseMonsterMove(wrappedPacket, data)) { logWrappedUncompressedFallbackUsed(); } else { logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE"); return; } } else { logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE"); return; } } auto entity = owner_.getEntityManager().getEntity(data.guid); if (!entity) { return; } if (data.hasDest) { glm::vec3 destCanonical = core::coords::serverToCanonical( glm::vec3(data.destX, data.destY, data.destZ)); float orientation = entity->getOrientation(); if (data.moveType == 4) { orientation = core::coords::serverToCanonicalYaw(data.facingAngle); } else if (data.moveType == 3) { auto target = owner_.getEntityManager().getEntity(data.facingTarget); if (target) { float dx = target->getX() - entity->getX(); float dy = target->getY() - entity->getY(); if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { orientation = std::atan2(-dy, dx); } } } else { float dx = destCanonical.x - entity->getX(); float dy = destCanonical.y - entity->getY(); if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { orientation = std::atan2(-dy, dx); } } if (data.moveType != 3) { glm::vec3 startCanonical = core::coords::serverToCanonical( glm::vec3(data.x, data.y, data.z)); float travelDx = destCanonical.x - startCanonical.x; float travelDy = destCanonical.y - startCanonical.y; float travelLen = std::sqrt(travelDx * travelDx + travelDy * travelDy); if (travelLen > 0.5f) { float travelAngle = std::atan2(-travelDy, travelDx); float diff = orientation - travelAngle; while (diff > static_cast(M_PI)) diff -= 2.0f * static_cast(M_PI); while (diff < -static_cast(M_PI)) diff += 2.0f * static_cast(M_PI); if (std::abs(diff) > static_cast(M_PI) * 0.5f) { orientation = travelAngle; } } } entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z, orientation, data.duration / 1000.0f); if (owner_.creatureMoveCallback_) { owner_.creatureMoveCallback_(data.guid, destCanonical.x, destCanonical.y, destCanonical.z, data.duration); } } else if (data.moveType == 1) { glm::vec3 posCanonical = core::coords::serverToCanonical( glm::vec3(data.x, data.y, data.z)); entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z, entity->getOrientation()); if (owner_.creatureMoveCallback_) { owner_.creatureMoveCallback_(data.guid, posCanonical.x, posCanonical.y, posCanonical.z, 0); } } else if (data.moveType == 4) { float orientation = core::coords::serverToCanonicalYaw(data.facingAngle); glm::vec3 posCanonical = core::coords::serverToCanonical( glm::vec3(data.x, data.y, data.z)); entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z, orientation); if (owner_.creatureMoveCallback_) { owner_.creatureMoveCallback_(data.guid, posCanonical.x, posCanonical.y, posCanonical.z, 0); } } else if (data.moveType == 3 && data.facingTarget != 0) { auto target = owner_.getEntityManager().getEntity(data.facingTarget); if (target) { float dx = target->getX() - entity->getX(); float dy = target->getY() - entity->getY(); if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { float orientation = std::atan2(-dy, dx); entity->setOrientation(orientation); } } } } void MovementHandler::handleMonsterMoveTransport(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 8 + 1 + 8 + 12) return; uint64_t moverGuid = packet.readUInt64(); /*uint8_t unk =*/ packet.readUInt8(); uint64_t transportGuid = packet.readUInt64(); float localX = packet.readFloat(); float localY = packet.readFloat(); float localZ = packet.readFloat(); auto entity = owner_.getEntityManager().getEntity(moverGuid); if (!entity) return; if (packet.getReadPos() + 5 > packet.getSize()) { if (owner_.transportManager_) { glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ)); owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, false, 0.0f); glm::vec3 worldPos = owner_.transportManager_->getPlayerWorldPosition(transportGuid, localCanonical); entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation()); if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_) owner_.creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0); } return; } /*uint32_t splineId =*/ packet.readUInt32(); uint8_t moveType = packet.readUInt8(); if (moveType == 1) { if (owner_.transportManager_) { glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ)); owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, false, 0.0f); glm::vec3 worldPos = owner_.transportManager_->getPlayerWorldPosition(transportGuid, localCanonical); entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation()); if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_) owner_.creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0); } return; } float facingAngle = entity->getOrientation(); if (moveType == 2) { if (packet.getReadPos() + 12 > packet.getSize()) return; float sx = packet.readFloat(), sy = packet.readFloat(), sz = packet.readFloat(); facingAngle = std::atan2(-(sy - localY), sx - localX); (void)sz; } else if (moveType == 3) { if (packet.getReadPos() + 8 > packet.getSize()) return; uint64_t tgtGuid = packet.readUInt64(); if (auto tgt = owner_.getEntityManager().getEntity(tgtGuid)) { float dx = tgt->getX() - entity->getX(); float dy = tgt->getY() - entity->getY(); if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) facingAngle = std::atan2(-dy, dx); } } else if (moveType == 4) { if (packet.getReadPos() + 4 > packet.getSize()) return; facingAngle = core::coords::serverToCanonicalYaw(packet.readFloat()); } if (packet.getReadPos() + 4 > packet.getSize()) return; uint32_t splineFlags = packet.readUInt32(); if (splineFlags & 0x00400000) { if (packet.getReadPos() + 5 > packet.getSize()) return; packet.readUInt8(); packet.readUInt32(); } if (packet.getReadPos() + 4 > packet.getSize()) return; uint32_t duration = packet.readUInt32(); if (splineFlags & 0x00000800) { if (packet.getReadPos() + 8 > packet.getSize()) return; packet.readFloat(); packet.readUInt32(); } if (packet.getReadPos() + 4 > packet.getSize()) return; uint32_t pointCount = packet.readUInt32(); constexpr uint32_t kMaxTransportSplinePoints = 1000; if (pointCount > kMaxTransportSplinePoints) { LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: pointCount=", pointCount, " clamped to ", kMaxTransportSplinePoints); pointCount = kMaxTransportSplinePoints; } float destLocalX = localX, destLocalY = localY, destLocalZ = localZ; bool hasDest = false; if (pointCount > 0) { const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0; if (uncompressed) { for (uint32_t i = 0; i < pointCount - 1; ++i) { if (packet.getReadPos() + 12 > packet.getSize()) break; packet.readFloat(); packet.readFloat(); packet.readFloat(); } if (packet.getReadPos() + 12 <= packet.getSize()) { destLocalX = packet.readFloat(); destLocalY = packet.readFloat(); destLocalZ = packet.readFloat(); hasDest = true; } } else { if (packet.getReadPos() + 12 <= packet.getSize()) { destLocalX = packet.readFloat(); destLocalY = packet.readFloat(); destLocalZ = packet.readFloat(); hasDest = true; } } } if (!owner_.transportManager_) { LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: TransportManager not available for mover 0x", std::hex, moverGuid, std::dec); return; } glm::vec3 startLocalCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ)); if (hasDest && duration > 0) { glm::vec3 destLocalCanonical = core::coords::serverToCanonical(glm::vec3(destLocalX, destLocalY, destLocalZ)); glm::vec3 destWorld = owner_.transportManager_->getPlayerWorldPosition(transportGuid, destLocalCanonical); if (moveType == 0) { float dx = destLocalCanonical.x - startLocalCanonical.x; float dy = destLocalCanonical.y - startLocalCanonical.y; if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) facingAngle = std::atan2(-dy, dx); } owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, destLocalCanonical, false, 0.0f); entity->startMoveTo(destWorld.x, destWorld.y, destWorld.z, facingAngle, duration / 1000.0f); if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_) owner_.creatureMoveCallback_(moverGuid, destWorld.x, destWorld.y, destWorld.z, duration); LOG_DEBUG("SMSG_MONSTER_MOVE_TRANSPORT: mover=0x", std::hex, moverGuid, " transport=0x", transportGuid, std::dec, " dur=", duration, "ms dest=(", destWorld.x, ",", destWorld.y, ",", destWorld.z, ")"); } else { glm::vec3 startWorld = owner_.transportManager_->getPlayerWorldPosition(transportGuid, startLocalCanonical); owner_.setTransportAttachment(moverGuid, entity->getType(), transportGuid, startLocalCanonical, false, 0.0f); entity->setPosition(startWorld.x, startWorld.y, startWorld.z, facingAngle); if (entity->getType() == ObjectType::UNIT && owner_.creatureMoveCallback_) owner_.creatureMoveCallback_(moverGuid, startWorld.x, startWorld.y, startWorld.z, 0); } } // ============================================================ // Teleport Handlers // ============================================================ void MovementHandler::handleTeleportAck(network::Packet& packet) { const bool taTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); if (packet.getSize() - packet.getReadPos() < (taTbc ? 8u : 4u)) { LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short"); return; } uint64_t guid = taTbc ? packet.readUInt64() : packet.readPackedGuid(); if (packet.getSize() - packet.getReadPos() < 4) return; uint32_t counter = packet.readUInt32(); const bool taNoFlags2 = isClassicLikeExpansion() || isActiveExpansion("tbc"); const size_t minMoveSz = taNoFlags2 ? (4 + 4 + 4 * 4) : (4 + 2 + 4 + 4 * 4); if (packet.getSize() - packet.getReadPos() < minMoveSz) { LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info"); return; } packet.readUInt32(); // moveFlags if (!taNoFlags2) packet.readUInt16(); // moveFlags2 (WotLK only) uint32_t moveTime = packet.readUInt32(); float serverX = packet.readFloat(); float serverY = packet.readFloat(); float serverZ = packet.readFloat(); float orientation = packet.readFloat(); LOG_INFO("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec, " counter=", counter, " pos=(", serverX, ", ", serverY, ", ", serverZ, ")"); glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ)); movementInfo.x = canonical.x; movementInfo.y = canonical.y; movementInfo.z = canonical.z; movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation); movementInfo.flags = 0; // Clear cast bar on teleport — SpellHandler owns the casting_ flag if (owner_.spellHandler_) owner_.spellHandler_->resetCastState(); if (owner_.socket) { network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK)); const bool legacyGuidAck = isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle"); if (legacyGuidAck) { ack.writeUInt64(owner_.playerGuid); } else { ack.writePackedGuid(owner_.playerGuid); } ack.writeUInt32(counter); ack.writeUInt32(moveTime); owner_.socket->send(ack); LOG_INFO("Sent MSG_MOVE_TELEPORT_ACK response"); } if (owner_.worldEntryCallback_) { owner_.worldEntryCallback_(owner_.currentMapId_, serverX, serverY, serverZ, false); } } void MovementHandler::handleNewWorld(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 20) { LOG_WARNING("SMSG_NEW_WORLD too short"); return; } uint32_t mapId = packet.readUInt32(); float serverX = packet.readFloat(); float serverY = packet.readFloat(); float serverZ = packet.readFloat(); float orientation = packet.readFloat(); LOG_INFO("SMSG_NEW_WORLD: mapId=", mapId, " pos=(", serverX, ", ", serverY, ", ", serverZ, ")", " orient=", orientation); const bool isSameMap = (mapId == owner_.currentMapId_); const bool isResurrection = owner_.resurrectPending_; if (isSameMap && isResurrection) { LOG_INFO("SMSG_NEW_WORLD same-map resurrection — skipping world reload"); glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ)); movementInfo.x = canonical.x; movementInfo.y = canonical.y; movementInfo.z = canonical.z; movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation); movementInfo.flags = 0; movementInfo.flags2 = 0; owner_.resurrectPending_ = false; owner_.resurrectRequestPending_ = false; owner_.releasedSpirit_ = false; owner_.playerDead_ = false; owner_.repopPending_ = false; owner_.pendingSpiritHealerGuid_ = 0; owner_.resurrectCasterGuid_ = 0; owner_.corpseMapId_ = 0; owner_.corpseGuid_ = 0; owner_.clearHostileAttackers(); owner_.stopAutoAttack(); owner_.tabCycleStale = true; owner_.resetCastState(); if (owner_.socket) { network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK)); owner_.socket->send(ack); LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK (resurrection)"); } return; } owner_.currentMapId_ = mapId; owner_.inInstance_ = false; if (owner_.socket) { owner_.socket->tracePacketsFor(std::chrono::seconds(12), "new_world"); } glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ)); movementInfo.x = canonical.x; movementInfo.y = canonical.y; movementInfo.z = canonical.z; movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation); movementInfo.flags = 0; movementInfo.flags2 = 0; serverMovementAllowed_ = true; owner_.resurrectPending_ = false; owner_.resurrectRequestPending_ = false; onTaxiFlight_ = false; taxiMountActive_ = false; taxiActivatePending_ = false; taxiClientActive_ = false; taxiClientPath_.clear(); taxiRecoverPending_ = false; taxiStartGrace_ = 0.0f; owner_.currentMountDisplayId_ = 0; taxiMountDisplayId_ = 0; if (owner_.mountCallback_) { owner_.mountCallback_(0); } for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) { if (guid == owner_.playerGuid) continue; if (entity->getType() == ObjectType::UNIT && owner_.creatureDespawnCallback_) { owner_.creatureDespawnCallback_(guid); } else if (entity->getType() == ObjectType::PLAYER && owner_.playerDespawnCallback_) { owner_.playerDespawnCallback_(guid); } else if (entity->getType() == ObjectType::GAMEOBJECT && owner_.gameObjectDespawnCallback_) { owner_.gameObjectDespawnCallback_(guid); } } owner_.otherPlayerVisibleItemEntries_.clear(); owner_.otherPlayerVisibleDirty_.clear(); otherPlayerMoveTimeMs_.clear(); if (owner_.spellHandler_) owner_.spellHandler_->clearUnitCastStates(); owner_.unitAurasCache_.clear(); owner_.clearCombatText(); owner_.getEntityManager().clear(); owner_.clearHostileAttackers(); owner_.worldStates_.clear(); owner_.gossipPois_.clear(); owner_.worldStateMapId_ = mapId; owner_.worldStateZoneId_ = 0; owner_.activeAreaTriggers_.clear(); owner_.areaTriggerCheckTimer_ = -5.0f; owner_.areaTriggerSuppressFirst_ = true; owner_.stopAutoAttack(); owner_.resetCastState(); if (owner_.socket) { network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK)); owner_.socket->send(ack); LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK"); } owner_.timeSinceLastPing = 0.0f; if (owner_.socket) { LOG_WARNING("World transfer keepalive: sending immediate ping after MSG_MOVE_WORLDPORT_ACK"); owner_.sendPing(); } if (owner_.worldEntryCallback_) { owner_.worldEntryCallback_(mapId, serverX, serverY, serverZ, isSameMap); } if (owner_.addonEventCallback_) { owner_.addonEventCallback_("PLAYER_ENTERING_WORLD", {"0"}); owner_.addonEventCallback_("ZONE_CHANGED_NEW_AREA", {}); } } // ============================================================ // Taxi / Flight Path Handlers // ============================================================ void MovementHandler::loadTaxiDbc() { if (taxiDbcLoaded_) return; taxiDbcLoaded_ = true; auto* am = core::Application::getInstance().getAssetManager(); if (!am || !am->isInitialized()) return; auto nodesDbc = am->loadDBC("TaxiNodes.dbc"); if (nodesDbc && nodesDbc->isLoaded()) { const auto* tnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiNodes") : nullptr; // Cache field indices before the loop const uint32_t tnIdField = tnL ? (*tnL)["ID"] : 0; const uint32_t tnMapField = tnL ? (*tnL)["MapID"] : 1; const uint32_t tnXField = tnL ? (*tnL)["X"] : 2; const uint32_t tnYField = tnL ? (*tnL)["Y"] : 3; const uint32_t tnZField = tnL ? (*tnL)["Z"] : 4; const uint32_t tnNameField = tnL ? (*tnL)["Name"] : 5; const uint32_t mountAllianceField = tnL ? (*tnL)["MountDisplayIdAlliance"] : 22; const uint32_t mountHordeField = tnL ? (*tnL)["MountDisplayIdHorde"] : 23; const uint32_t mountAllianceFB = tnL ? (*tnL)["MountDisplayIdAllianceFallback"] : 20; const uint32_t mountHordeFB = tnL ? (*tnL)["MountDisplayIdHordeFallback"] : 21; uint32_t fieldCount = nodesDbc->getFieldCount(); for (uint32_t i = 0; i < nodesDbc->getRecordCount(); i++) { TaxiNode node; node.id = nodesDbc->getUInt32(i, tnIdField); node.mapId = nodesDbc->getUInt32(i, tnMapField); node.x = nodesDbc->getFloat(i, tnXField); node.y = nodesDbc->getFloat(i, tnYField); node.z = nodesDbc->getFloat(i, tnZField); node.name = nodesDbc->getString(i, tnNameField); if (fieldCount > mountHordeField) { node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceField); node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeField); if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount > mountHordeFB) { node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceFB); node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeFB); } } uint32_t nodeId = node.id; if (nodeId > 0) { taxiNodes_[nodeId] = std::move(node); } if (nodeId == 195) { std::string fields; for (uint32_t f = 0; f < fieldCount; f++) { fields += std::to_string(f) + ":" + std::to_string(nodesDbc->getUInt32(i, f)) + " "; } LOG_INFO("TaxiNodes[195] fields: ", fields); } } LOG_INFO("Loaded ", taxiNodes_.size(), " taxi nodes from TaxiNodes.dbc"); } else { LOG_WARNING("Could not load TaxiNodes.dbc"); } auto pathDbc = am->loadDBC("TaxiPath.dbc"); if (pathDbc && pathDbc->isLoaded()) { const auto* tpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPath") : nullptr; const uint32_t tpIdField = tpL ? (*tpL)["ID"] : 0; const uint32_t tpFromField = tpL ? (*tpL)["FromNode"] : 1; const uint32_t tpToField = tpL ? (*tpL)["ToNode"] : 2; const uint32_t tpCostField = tpL ? (*tpL)["Cost"] : 3; for (uint32_t i = 0; i < pathDbc->getRecordCount(); i++) { TaxiPathEdge edge; edge.pathId = pathDbc->getUInt32(i, tpIdField); edge.fromNode = pathDbc->getUInt32(i, tpFromField); edge.toNode = pathDbc->getUInt32(i, tpToField); edge.cost = pathDbc->getUInt32(i, tpCostField); taxiPathEdges_.push_back(edge); } LOG_INFO("Loaded ", taxiPathEdges_.size(), " taxi path edges from TaxiPath.dbc"); } else { LOG_WARNING("Could not load TaxiPath.dbc"); } auto pathNodeDbc = am->loadDBC("TaxiPathNode.dbc"); if (pathNodeDbc && pathNodeDbc->isLoaded()) { const auto* tpnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPathNode") : nullptr; const uint32_t tpnIdField = tpnL ? (*tpnL)["ID"] : 0; const uint32_t tpnPathField = tpnL ? (*tpnL)["PathID"] : 1; const uint32_t tpnIndexField = tpnL ? (*tpnL)["NodeIndex"] : 2; const uint32_t tpnMapField = tpnL ? (*tpnL)["MapID"] : 3; const uint32_t tpnXField = tpnL ? (*tpnL)["X"] : 4; const uint32_t tpnYField = tpnL ? (*tpnL)["Y"] : 5; const uint32_t tpnZField = tpnL ? (*tpnL)["Z"] : 6; for (uint32_t i = 0; i < pathNodeDbc->getRecordCount(); i++) { TaxiPathNode node; node.id = pathNodeDbc->getUInt32(i, tpnIdField); node.pathId = pathNodeDbc->getUInt32(i, tpnPathField); node.nodeIndex = pathNodeDbc->getUInt32(i, tpnIndexField); node.mapId = pathNodeDbc->getUInt32(i, tpnMapField); node.x = pathNodeDbc->getFloat(i, tpnXField); node.y = pathNodeDbc->getFloat(i, tpnYField); node.z = pathNodeDbc->getFloat(i, tpnZField); taxiPathNodes_[node.pathId].push_back(node); } for (auto& [pathId, nodes] : taxiPathNodes_) { std::sort(nodes.begin(), nodes.end(), [](const TaxiPathNode& a, const TaxiPathNode& b) { return a.nodeIndex < b.nodeIndex; }); } LOG_INFO("Loaded ", pathNodeDbc->getRecordCount(), " taxi path waypoints from TaxiPathNode.dbc"); } else { LOG_WARNING("Could not load TaxiPathNode.dbc"); } } void MovementHandler::handleShowTaxiNodes(network::Packet& packet) { ShowTaxiNodesData data; if (!ShowTaxiNodesParser::parse(packet, data)) { LOG_WARNING("Failed to parse SMSG_SHOWTAXINODES"); return; } loadTaxiDbc(); if (taxiMaskInitialized_) { for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) { uint32_t newBits = data.nodeMask[i] & ~knownTaxiMask_[i]; if (newBits == 0) continue; for (uint32_t bit = 0; bit < 32; ++bit) { if (newBits & (1u << bit)) { uint32_t nodeId = i * 32 + bit + 1; auto it = taxiNodes_.find(nodeId); if (it != taxiNodes_.end()) { owner_.addSystemChatMessage("Discovered flight path: " + it->second.name); } } } } } for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) { knownTaxiMask_[i] = data.nodeMask[i]; } taxiMaskInitialized_ = true; currentTaxiData_ = data; taxiNpcGuid_ = data.npcGuid; taxiWindowOpen_ = true; owner_.closeGossip(); buildTaxiCostMap(); auto it = taxiNodes_.find(data.nearestNode); if (it != taxiNodes_.end()) { LOG_INFO("Taxi node ", data.nearestNode, " mounts: A=", it->second.mountDisplayIdAlliance, " H=", it->second.mountDisplayIdHorde); } LOG_INFO("Taxi window opened, nearest node=", data.nearestNode); } void MovementHandler::applyTaxiMountForCurrentNode() { if (taxiMountActive_ || !owner_.mountCallback_) return; auto it = taxiNodes_.find(currentTaxiData_.nearestNode); if (it == taxiNodes_.end()) { bool isAlliance = true; switch (owner_.playerRace_) { case Race::ORC: case Race::UNDEAD: case Race::TAUREN: case Race::TROLL: case Race::GOBLIN: case Race::BLOOD_ELF: isAlliance = false; break; default: break; } uint32_t mountId = isAlliance ? 1210u : 1310u; taxiMountDisplayId_ = mountId; taxiMountActive_ = true; LOG_INFO("Taxi mount fallback (node ", currentTaxiData_.nearestNode, " not in DBC): displayId=", mountId); owner_.mountCallback_(mountId); return; } bool isAlliance = true; switch (owner_.playerRace_) { case Race::ORC: case Race::UNDEAD: case Race::TAUREN: case Race::TROLL: case Race::GOBLIN: case Race::BLOOD_ELF: isAlliance = false; break; default: isAlliance = true; break; } uint32_t mountId = isAlliance ? it->second.mountDisplayIdAlliance : it->second.mountDisplayIdHorde; if (mountId == 541) mountId = 0; if (mountId == 0) { mountId = isAlliance ? it->second.mountDisplayIdHorde : it->second.mountDisplayIdAlliance; if (mountId == 541) mountId = 0; } if (mountId == 0) { auto& app = core::Application::getInstance(); uint32_t gryphonId = app.getGryphonDisplayId(); uint32_t wyvernId = app.getWyvernDisplayId(); if (isAlliance && gryphonId != 0) mountId = gryphonId; if (!isAlliance && wyvernId != 0) mountId = wyvernId; if (mountId == 0) { mountId = (isAlliance ? wyvernId : gryphonId); } } if (mountId == 0) { if (it->second.mountDisplayIdAlliance != 0) mountId = it->second.mountDisplayIdAlliance; else if (it->second.mountDisplayIdHorde != 0) mountId = it->second.mountDisplayIdHorde; } if (mountId == 0) { static const uint32_t kAllianceTaxiDisplays[] = {1210u, 1211u, 1212u, 1213u}; static const uint32_t kHordeTaxiDisplays[] = {1310u, 1311u, 1312u}; mountId = isAlliance ? kAllianceTaxiDisplays[0] : kHordeTaxiDisplays[0]; } if (mountId == 0) { mountId = isAlliance ? 30412u : 30413u; } if (mountId != 0) { taxiMountDisplayId_ = mountId; taxiMountActive_ = true; LOG_INFO("Taxi mount apply: displayId=", mountId); owner_.mountCallback_(mountId); } } void MovementHandler::startClientTaxiPath(const std::vector& pathNodes) { taxiClientPath_.clear(); taxiClientIndex_ = 0; taxiClientActive_ = false; taxiClientSegmentProgress_ = 0.0f; for (size_t i = 0; i + 1 < pathNodes.size(); i++) { uint32_t fromNode = pathNodes[i]; uint32_t toNode = pathNodes[i + 1]; uint32_t pathId = 0; for (const auto& edge : taxiPathEdges_) { if (edge.fromNode == fromNode && edge.toNode == toNode) { pathId = edge.pathId; break; } } if (pathId == 0) { LOG_WARNING("No taxi path found from node ", fromNode, " to ", toNode); continue; } auto pathIt = taxiPathNodes_.find(pathId); if (pathIt != taxiPathNodes_.end()) { for (const auto& wpNode : pathIt->second) { glm::vec3 serverPos(wpNode.x, wpNode.y, wpNode.z); glm::vec3 canonical = core::coords::serverToCanonical(serverPos); taxiClientPath_.push_back(canonical); } } else { LOG_WARNING("No spline waypoints found for taxi pathId ", pathId); } } if (taxiClientPath_.size() < 2) { taxiClientPath_.clear(); for (uint32_t nodeId : pathNodes) { auto nodeIt = taxiNodes_.find(nodeId); if (nodeIt == taxiNodes_.end()) continue; glm::vec3 serverPos(nodeIt->second.x, nodeIt->second.y, nodeIt->second.z); taxiClientPath_.push_back(core::coords::serverToCanonical(serverPos)); } } if (taxiClientPath_.size() < 2) { LOG_WARNING("Taxi path too short: ", taxiClientPath_.size(), " waypoints"); return; } glm::vec3 start = taxiClientPath_[0]; glm::vec3 dir(0.0f); float dirLenSq = 0.0f; for (size_t i = 1; i < taxiClientPath_.size(); i++) { dir = taxiClientPath_[i] - start; dirLenSq = glm::dot(dir, dir); if (dirLenSq >= 1e-6f) { break; } } float initialOrientation = movementInfo.orientation; float initialRenderYaw = movementInfo.orientation; float initialPitch = 0.0f; float initialRoll = 0.0f; if (dirLenSq >= 1e-6f) { initialOrientation = std::atan2(dir.y, dir.x); glm::vec3 renderDir = core::coords::canonicalToRender(dir); initialRenderYaw = std::atan2(renderDir.y, renderDir.x); glm::vec3 dirNorm = dir * glm::inversesqrt(dirLenSq); initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f)); } movementInfo.x = start.x; movementInfo.y = start.y; movementInfo.z = start.z; movementInfo.orientation = initialOrientation; sanitizeMovementForTaxi(); auto playerEntity = owner_.getEntityManager().getEntity(owner_.playerGuid); if (playerEntity) { playerEntity->setPosition(start.x, start.y, start.z, initialOrientation); } if (owner_.taxiOrientationCallback_) { owner_.taxiOrientationCallback_(initialRenderYaw, initialPitch, initialRoll); } LOG_INFO("Taxi flight started with ", taxiClientPath_.size(), " spline waypoints"); taxiClientActive_ = true; } void MovementHandler::updateClientTaxi(float deltaTime) { if (!taxiClientActive_ || taxiClientPath_.size() < 2) return; auto playerEntity = owner_.getEntityManager().getEntity(owner_.playerGuid); auto finishTaxiFlight = [&]() { if (!taxiClientPath_.empty()) { const auto& landingPos = taxiClientPath_.back(); if (playerEntity) { playerEntity->setPosition(landingPos.x, landingPos.y, landingPos.z, movementInfo.orientation); } movementInfo.x = landingPos.x; movementInfo.y = landingPos.y; movementInfo.z = landingPos.z; LOG_INFO("Taxi landing: snapped to final waypoint (", landingPos.x, ", ", landingPos.y, ", ", landingPos.z, ")"); } taxiClientActive_ = false; onTaxiFlight_ = false; taxiLandingCooldown_ = 2.0f; if (taxiMountActive_ && owner_.mountCallback_) { owner_.mountCallback_(0); } taxiMountActive_ = false; taxiMountDisplayId_ = 0; owner_.currentMountDisplayId_ = 0; taxiClientPath_.clear(); taxiRecoverPending_ = false; movementInfo.flags = 0; movementInfo.flags2 = 0; if (owner_.socket) { sendMovement(Opcode::MSG_MOVE_STOP); sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } LOG_INFO("Taxi flight landed (client path)"); }; if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { finishTaxiFlight(); return; } float remainingDistance = taxiClientSegmentProgress_ + (taxiClientSpeed_ * deltaTime); glm::vec3 start(0.0f); glm::vec3 end(0.0f); glm::vec3 dir(0.0f); float segmentLen = 0.0f; float t = 0.0f; while (true) { if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { finishTaxiFlight(); return; } start = taxiClientPath_[taxiClientIndex_]; end = taxiClientPath_[taxiClientIndex_ + 1]; dir = end - start; float segLenSq = glm::dot(dir, dir); if (segLenSq < 1e-4f) { taxiClientIndex_++; continue; } segmentLen = std::sqrt(segLenSq); if (remainingDistance >= segmentLen) { remainingDistance -= segmentLen; taxiClientIndex_++; taxiClientSegmentProgress_ = 0.0f; continue; } taxiClientSegmentProgress_ = remainingDistance; t = taxiClientSegmentProgress_ / segmentLen; break; } glm::vec3 p0 = (taxiClientIndex_ > 0) ? taxiClientPath_[taxiClientIndex_ - 1] : start; glm::vec3 p1 = start; glm::vec3 p2 = end; glm::vec3 p3 = (taxiClientIndex_ + 2 < taxiClientPath_.size()) ? taxiClientPath_[taxiClientIndex_ + 2] : end; float t2 = t * t; float t3 = t2 * t; glm::vec3 nextPos = 0.5f * ( (2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3 ); glm::vec3 tangent = 0.5f * ( (-p0 + p2) + 2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t + 3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2 ); float tangentLenSq = glm::dot(tangent, tangent); if (tangentLenSq < 1e-8f) { tangent = dir; tangentLenSq = glm::dot(tangent, tangent); if (tangentLenSq < 1e-8f) { tangent = glm::vec3(std::cos(movementInfo.orientation), std::sin(movementInfo.orientation), 0.0f); tangentLenSq = 1.0f; // unit vector } } float targetOrientation = std::atan2(tangent.y, tangent.x); glm::vec3 tangentNorm = tangent * glm::inversesqrt(std::max(tangentLenSq, 1e-8f)); float pitch = std::asin(std::clamp(tangentNorm.z, -1.0f, 1.0f)); float currentOrientation = movementInfo.orientation; float orientDiff = targetOrientation - currentOrientation; while (orientDiff > 3.14159265f) orientDiff -= 6.28318530f; while (orientDiff < -3.14159265f) orientDiff += 6.28318530f; float roll = -orientDiff * 2.5f; roll = std::clamp(roll, -0.7f, 0.7f); float smoothOrientation = currentOrientation + orientDiff * std::min(1.0f, deltaTime * 3.0f); if (playerEntity) { playerEntity->setPosition(nextPos.x, nextPos.y, nextPos.z, smoothOrientation); } movementInfo.x = nextPos.x; movementInfo.y = nextPos.y; movementInfo.z = nextPos.z; movementInfo.orientation = smoothOrientation; if (owner_.taxiOrientationCallback_) { glm::vec3 renderTangent = core::coords::canonicalToRender(tangent); float renderYaw = std::atan2(renderTangent.y, renderTangent.x); owner_.taxiOrientationCallback_(renderYaw, pitch, roll); } } void MovementHandler::handleActivateTaxiReply(network::Packet& packet) { ActivateTaxiReplyData data; if (!ActivateTaxiReplyParser::parse(packet, data)) { LOG_WARNING("Failed to parse SMSG_ACTIVATETAXIREPLY"); return; } if (!taxiActivatePending_) { LOG_DEBUG("Ignoring stray taxi reply: result=", data.result); return; } if (data.result == 0) { if (onTaxiFlight_ && !taxiActivatePending_) { return; } onTaxiFlight_ = true; taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f); sanitizeMovementForTaxi(); taxiWindowOpen_ = false; taxiActivatePending_ = false; taxiActivateTimer_ = 0.0f; applyTaxiMountForCurrentNode(); if (owner_.socket) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } LOG_INFO("Taxi flight started!"); } else { if (onTaxiFlight_ || taxiClientActive_) { LOG_WARNING("Ignoring stale taxi failure reply while flight is active: result=", data.result); taxiActivatePending_ = false; taxiActivateTimer_ = 0.0f; return; } LOG_WARNING("Taxi activation failed, result=", data.result); owner_.addSystemChatMessage("Cannot take that flight path."); taxiActivatePending_ = false; taxiActivateTimer_ = 0.0f; if (taxiMountActive_ && owner_.mountCallback_) { owner_.mountCallback_(0); } taxiMountActive_ = false; taxiMountDisplayId_ = 0; onTaxiFlight_ = false; } } void MovementHandler::closeTaxi() { taxiWindowOpen_ = false; if (taxiActivatePending_ || onTaxiFlight_ || taxiClientActive_) { return; } if (taxiMountActive_ && owner_.mountCallback_) { owner_.mountCallback_(0); } taxiMountActive_ = false; taxiMountDisplayId_ = 0; taxiActivatePending_ = false; onTaxiFlight_ = false; taxiLandingCooldown_ = 2.0f; } void MovementHandler::buildTaxiCostMap() { taxiCostMap_.clear(); uint32_t startNode = currentTaxiData_.nearestNode; if (startNode == 0) return; struct AdjEntry { uint32_t node; uint32_t cost; }; std::unordered_map> adj; for (const auto& edge : taxiPathEdges_) { adj[edge.fromNode].push_back({edge.toNode, edge.cost}); } std::deque queue; queue.push_back(startNode); taxiCostMap_[startNode] = 0; while (!queue.empty()) { uint32_t cur = queue.front(); queue.pop_front(); for (const auto& next : adj[cur]) { if (taxiCostMap_.find(next.node) == taxiCostMap_.end()) { taxiCostMap_[next.node] = taxiCostMap_[cur] + next.cost; queue.push_back(next.node); } } } } uint32_t MovementHandler::getTaxiCostTo(uint32_t destNodeId) const { auto it = taxiCostMap_.find(destNodeId); return (it != taxiCostMap_.end()) ? it->second : 0; } void MovementHandler::activateTaxi(uint32_t destNodeId) { if (!owner_.socket || owner_.state != WorldState::IN_WORLD) return; if (taxiActivatePending_ || onTaxiFlight_) { return; } uint32_t startNode = currentTaxiData_.nearestNode; if (startNode == 0 || destNodeId == 0 || startNode == destNodeId) return; if (owner_.isMounted()) { LOG_INFO("Taxi activate: dismounting current mount"); if (owner_.mountCallback_) owner_.mountCallback_(0); owner_.currentMountDisplayId_ = 0; dismount(); } { auto destIt = taxiNodes_.find(destNodeId); if (destIt != taxiNodes_.end() && !destIt->second.name.empty()) { taxiDestName_ = destIt->second.name; owner_.addSystemChatMessage("Requesting flight to " + destIt->second.name + "..."); } else { taxiDestName_.clear(); owner_.addSystemChatMessage("Taxi: requesting flight..."); } } // BFS to find path from startNode to destNodeId std::unordered_map> adj; for (const auto& edge : taxiPathEdges_) { adj[edge.fromNode].push_back(edge.toNode); } std::unordered_map parent; std::deque 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 path; for (uint32_t n = destNodeId; n != startNode; n = parent[n]) { path.push_back(n); } path.push_back(startNode); std::reverse(path.begin(), path.end()); LOG_INFO("Taxi path: ", path.size(), " nodes, from ", startNode, " to ", destNodeId); LOG_INFO("Taxi activate: npc=0x", std::hex, taxiNpcGuid_, std::dec, " start=", startNode, " dest=", destNodeId, " pathLen=", path.size()); if (!path.empty()) { std::string pathStr; for (size_t i = 0; i < path.size(); i++) { pathStr += std::to_string(path[i]); if (i + 1 < path.size()) pathStr += "->"; } LOG_INFO("Taxi path nodes: ", pathStr); } uint32_t totalCost = getTaxiCostTo(destNodeId); LOG_INFO("Taxi activate: start=", startNode, " dest=", destNodeId, " cost=", totalCost); auto basicPkt = ActivateTaxiPacket::build(taxiNpcGuid_, startNode, destNodeId); owner_.socket->send(basicPkt); taxiWindowOpen_ = false; taxiActivatePending_ = true; taxiActivateTimer_ = 0.0f; taxiStartGrace_ = 2.0f; if (!onTaxiFlight_) { onTaxiFlight_ = true; sanitizeMovementForTaxi(); applyTaxiMountForCurrentNode(); } if (owner_.socket) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } if (owner_.taxiPrecacheCallback_) { std::vector previewPath; for (size_t i = 0; i + 1 < path.size(); i++) { uint32_t fromNode = path[i]; uint32_t toNode = path[i + 1]; uint32_t pathId = 0; for (const auto& edge : taxiPathEdges_) { if (edge.fromNode == fromNode && edge.toNode == toNode) { pathId = edge.pathId; break; } } if (pathId == 0) continue; auto pathIt = taxiPathNodes_.find(pathId); if (pathIt != taxiPathNodes_.end()) { for (const auto& wpNode : pathIt->second) { glm::vec3 serverPos(wpNode.x, wpNode.y, wpNode.z); glm::vec3 canonical = core::coords::serverToCanonical(serverPos); previewPath.push_back(canonical); } } } if (previewPath.size() >= 2) { owner_.taxiPrecacheCallback_(previewPath); } } if (owner_.taxiFlightStartCallback_) { owner_.taxiFlightStartCallback_(); } startClientTaxiPath(path); } // ============================================================ // Area Trigger Detection (moved from GameHandler) // ============================================================ void MovementHandler::loadAreaTriggerDbc() { if (owner_.areaTriggerDbcLoaded_) return; owner_.areaTriggerDbcLoaded_ = true; auto* am = core::Application::getInstance().getAssetManager(); if (!am || !am->isInitialized()) return; auto dbc = am->loadDBC("AreaTrigger.dbc"); if (!dbc || !dbc->isLoaded()) { LOG_WARNING("Failed to load AreaTrigger.dbc"); return; } owner_.areaTriggers_.reserve(dbc->getRecordCount()); for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { GameHandler::AreaTriggerEntry at; at.id = dbc->getUInt32(i, 0); at.mapId = dbc->getUInt32(i, 1); // DBC stores positions in server/wire format (X=west, Y=north) — swap to canonical at.x = dbc->getFloat(i, 3); // canonical X (north) = DBC field 3 (Y_wire) at.y = dbc->getFloat(i, 2); // canonical Y (west) = DBC field 2 (X_wire) at.z = dbc->getFloat(i, 4); at.radius = dbc->getFloat(i, 5); at.boxLength = dbc->getFloat(i, 6); at.boxWidth = dbc->getFloat(i, 7); at.boxHeight = dbc->getFloat(i, 8); at.boxYaw = dbc->getFloat(i, 9); owner_.areaTriggers_.push_back(at); } LOG_WARNING("Loaded ", owner_.areaTriggers_.size(), " area triggers from AreaTrigger.dbc"); } void MovementHandler::checkAreaTriggers() { if (!owner_.isInWorld()) return; if (onTaxiFlight_ || taxiClientActive_) return; loadAreaTriggerDbc(); if (owner_.areaTriggers_.empty()) return; const float px = movementInfo.x; const float py = movementInfo.y; const float pz = movementInfo.z; // On first check after map transfer, just mark which triggers we're inside // without firing them — prevents exit portal from immediately sending us back bool suppressFirst = owner_.areaTriggerSuppressFirst_; if (suppressFirst) { owner_.areaTriggerSuppressFirst_ = false; } for (const auto& at : owner_.areaTriggers_) { if (at.mapId != owner_.currentMapId_) continue; bool inside = false; if (at.radius > 0.0f) { // Sphere trigger — use actual radius, with small floor for very tiny triggers float effectiveRadius = std::max(at.radius, 3.0f); float dx = px - at.x; float dy = py - at.y; float dz = pz - at.z; float distSq = dx * dx + dy * dy + dz * dz; inside = (distSq <= effectiveRadius * effectiveRadius); } else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) { // Box trigger — use actual size, with small floor for tiny triggers float boxMin = 4.0f; float effLength = std::max(at.boxLength, boxMin); float effWidth = std::max(at.boxWidth, boxMin); float effHeight = std::max(at.boxHeight, boxMin); float dx = px - at.x; float dy = py - at.y; float dz = pz - at.z; // Rotate into box-local space float cosYaw = std::cos(-at.boxYaw); float sinYaw = std::sin(-at.boxYaw); float localX = dx * cosYaw - dy * sinYaw; float localY = dx * sinYaw + dy * cosYaw; inside = (std::abs(localX) <= effLength * 0.5f && std::abs(localY) <= effWidth * 0.5f && std::abs(dz) <= effHeight * 0.5f); } if (inside) { if (owner_.activeAreaTriggers_.count(at.id) == 0) { owner_.activeAreaTriggers_.insert(at.id); if (suppressFirst) { // After map transfer: mark triggers we're inside of, but don't fire them. // This prevents the exit portal from immediately sending us back. LOG_WARNING("AreaTrigger suppressed (post-transfer): AT", at.id); } else { // Temporarily move player to trigger center so the server's distance // check passes, then restore to actual position so the server doesn't // persist the fake position on disconnect. float savedX = movementInfo.x, savedY = movementInfo.y, savedZ = movementInfo.z; movementInfo.x = at.x; movementInfo.y = at.y; movementInfo.z = at.z; sendMovement(Opcode::MSG_MOVE_HEARTBEAT); network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER)); pkt.writeUInt32(at.id); owner_.socket->send(pkt); LOG_WARNING("Fired CMSG_AREATRIGGER: id=", at.id, " at (", at.x, ", ", at.y, ", ", at.z, ")"); // Restore actual player position movementInfo.x = savedX; movementInfo.y = savedY; movementInfo.z = savedZ; sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } } } else { // Player left the trigger — allow re-fire on re-entry owner_.activeAreaTriggers_.erase(at.id); } } } // ============================================================ // Transport Attachments (moved from GameHandler) // ============================================================ void MovementHandler::setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid, const glm::vec3& localOffset, bool hasLocalOrientation, float localOrientation) { if (childGuid == 0 || transportGuid == 0) { return; } GameHandler::TransportAttachment& attachment = owner_.transportAttachments_[childGuid]; attachment.type = type; attachment.transportGuid = transportGuid; attachment.localOffset = localOffset; attachment.hasLocalOrientation = hasLocalOrientation; attachment.localOrientation = localOrientation; } void MovementHandler::clearTransportAttachment(uint64_t childGuid) { if (childGuid == 0) { return; } owner_.transportAttachments_.erase(childGuid); } void MovementHandler::updateAttachedTransportChildren(float /*deltaTime*/) { if (!owner_.transportManager_ || owner_.transportAttachments_.empty()) { return; } constexpr float kPosEpsilonSq = 0.0001f; constexpr float kOriEpsilon = 0.001f; std::vector stale; stale.reserve(8); for (const auto& [childGuid, attachment] : owner_.transportAttachments_) { auto entity = owner_.getEntityManager().getEntity(childGuid); if (!entity) { stale.push_back(childGuid); continue; } ActiveTransport* transport = owner_.transportManager_->getTransport(attachment.transportGuid); if (!transport) { continue; } glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition( attachment.transportGuid, attachment.localOffset); float composedOrientation = entity->getOrientation(); if (attachment.hasLocalOrientation) { float baseYaw = transport->hasServerYaw ? transport->serverYaw : 0.0f; composedOrientation = baseYaw + attachment.localOrientation; } glm::vec3 oldPos(entity->getX(), entity->getY(), entity->getZ()); float oldOrientation = entity->getOrientation(); glm::vec3 delta = composed - oldPos; const bool positionChanged = glm::dot(delta, delta) > kPosEpsilonSq; const bool orientationChanged = std::abs(composedOrientation - oldOrientation) > kOriEpsilon; if (!positionChanged && !orientationChanged) { continue; } entity->setPosition(composed.x, composed.y, composed.z, composedOrientation); if (attachment.type == ObjectType::UNIT) { if (owner_.creatureMoveCallback_) { owner_.creatureMoveCallback_(childGuid, composed.x, composed.y, composed.z, 0); } } else if (attachment.type == ObjectType::GAMEOBJECT) { if (owner_.gameObjectMoveCallback_) { owner_.gameObjectMoveCallback_(childGuid, composed.x, composed.y, composed.z, composedOrientation); } } } for (uint64_t guid : stale) { owner_.transportAttachments_.erase(guid); } } // ============================================================ // Follow target (moved from GameHandler) // ============================================================ void MovementHandler::followTarget() { if (owner_.state != WorldState::IN_WORLD) { LOG_WARNING("Cannot follow: not in world"); return; } if (owner_.targetGuid == 0) { owner_.addSystemChatMessage("You must target someone to follow."); return; } auto target = owner_.getTarget(); if (!target) { owner_.addSystemChatMessage("Invalid target."); return; } // Set follow target owner_.followTargetGuid_ = owner_.targetGuid; // Initialize render-space position from entity's canonical coords owner_.followRenderPos_ = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ())); // Tell camera controller to start auto-following if (owner_.autoFollowCallback_) { owner_.autoFollowCallback_(&owner_.followRenderPos_); } // Get target name std::string targetName = "Target"; if (target->getType() == ObjectType::PLAYER) { auto player = std::static_pointer_cast(target); if (!player->getName().empty()) { targetName = player->getName(); } } else if (target->getType() == ObjectType::UNIT) { auto unit = std::static_pointer_cast(target); targetName = unit->getName(); } owner_.addSystemChatMessage("Now following " + targetName + "."); LOG_INFO("Following target: ", targetName, " (GUID: 0x", std::hex, owner_.targetGuid, std::dec, ")"); owner_.fireAddonEvent("AUTOFOLLOW_BEGIN", {}); } void MovementHandler::cancelFollow() { if (owner_.followTargetGuid_ == 0) { return; } owner_.followTargetGuid_ = 0; if (owner_.autoFollowCallback_) { owner_.autoFollowCallback_(nullptr); } owner_.addSystemChatMessage("You stop following."); owner_.fireAddonEvent("AUTOFOLLOW_END", {}); } } // namespace game } // namespace wowee