diff --git a/include/core/application.hpp b/include/core/application.hpp index bf8eba67..0c7ca61e 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -188,7 +188,8 @@ private: std::unordered_map creatureModelIds_; // guid → loaded modelId std::unordered_map creatureRenderPosCache_; // guid -> last synced render position std::unordered_map creatureWasMoving_; // guid -> previous-frame movement state - std::unordered_map creatureSwimmingState_; // guid -> currently in swim mode + std::unordered_map creatureSwimmingState_; // guid -> currently in swim mode (SWIMMING flag) + std::unordered_map creatureWalkingState_; // guid -> walking (WALKING flag, selects Walk(4) vs Run(5)) std::unordered_set creatureWeaponsAttached_; // guid set when NPC virtual weapons attached std::unordered_map creatureWeaponAttachAttempts_; // guid -> attach attempts std::unordered_map modelIdIsWolfLike_; // modelId → cached wolf/worg check diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 457f9870..8e3420c5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -637,10 +637,15 @@ public: using SpellCastAnimCallback = std::function; void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); } - // Unit animation hint: signal jump (animId=38) or swim (animId=42) for other players/NPCs + // Unit animation hint: signal jump (animId=38) for other players/NPCs using UnitAnimHintCallback = std::function; void setUnitAnimHintCallback(UnitAnimHintCallback cb) { unitAnimHintCallback_ = std::move(cb); } + // Unit move-flags callback: fired on every MSG_MOVE_* for other players with the raw flags field. + // Drives Walk(4) vs Run(5) selection and swim state initialization from heartbeat packets. + using UnitMoveFlagsCallback = std::function; + void setUnitMoveFlagsCallback(UnitMoveFlagsCallback cb) { unitMoveFlagsCallback_ = std::move(cb); } + // NPC swing callback (plays attack animation on NPC) using NpcSwingCallback = std::function; void setNpcSwingCallback(NpcSwingCallback cb) { npcSwingCallback_ = std::move(cb); } @@ -2268,6 +2273,7 @@ private: MeleeSwingCallback meleeSwingCallback_; SpellCastAnimCallback spellCastAnimCallback_; UnitAnimHintCallback unitAnimHintCallback_; + UnitMoveFlagsCallback unitMoveFlagsCallback_; NpcSwingCallback npcSwingCallback_; NpcGreetingCallback npcGreetingCallback_; NpcFarewellCallback npcFarewellCallback_; diff --git a/src/core/application.cpp b/src/core/application.cpp index 15e4d326..ca692dd6 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -751,6 +751,7 @@ void Application::logoutToLogin() { creatureWeaponAttachAttempts_.clear(); creatureWasMoving_.clear(); creatureSwimmingState_.clear(); + creatureWalkingState_.clear(); deadCreatureGuids_.clear(); nonRenderableCreatureDisplayIds_.clear(); creaturePermanentFailureGuids_.clear(); @@ -1477,11 +1478,13 @@ void Application::update(float deltaTime) { } posIt->second = renderPos; - // Drive movement animation: Run/Swim (5/42) when moving, Stand/SwimIdle (0/41) when idle. + // Drive movement animation: Walk/Run/Swim (4/5/42) when moving, + // Stand/SwimIdle (0/41) when idle. Walk(4) selected when WALKING flag is set. // WoW M2 animation IDs: 4=Walk, 5=Run, 41=SwimIdle, 42=Swim. // Only switch on transitions to avoid resetting animation time. // Don't override Death (1) animation. const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0; + const bool isWalkingNow = creatureWalkingState_.count(guid) > 0; bool prevMoving = creatureWasMoving_[guid]; if (isMovingNow != prevMoving) { creatureWasMoving_[guid] = isMovingNow; @@ -1490,7 +1493,7 @@ void Application::update(float deltaTime) { if (!gotState || curAnimId != 1 /*Death*/) { uint32_t targetAnim; if (isMovingNow) - targetAnim = isSwimmingNow ? 42u : 5u; // Swim vs Run + targetAnim = isSwimmingNow ? 42u : (isWalkingNow ? 4u : 5u); // Swim/Walk/Run else targetAnim = isSwimmingNow ? 41u : 0u; // SwimIdle vs Stand charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true); @@ -2777,20 +2780,10 @@ void Application::setupUICallbacks() { } }); - // Unit animation hint callback — play jump (38) or swim (42) on other players/NPCs. - // animId=42 (Swim): marks entity as swimming; per-frame sync will use SwimIdle(41) when stopped. - // animId=0: clears swim state (MSG_MOVE_STOP_SWIM); per-frame sync reverts to Stand(0). + // Unit animation hint callback — plays jump (38=JumpMid) animation on other players/NPCs. + // Swim/walking state is now authoritative from the move-flags callback below. // animId=38 (JumpMid): airborne jump animation; land detection is via per-frame sync. gameHandler->setUnitAnimHintCallback([this](uint64_t guid, uint32_t animId) { - // Track swim state regardless of whether the instance is visible yet. - if (animId == 42u) { - creatureSwimmingState_[guid] = true; - } else if (animId == 0u) { - creatureSwimmingState_.erase(guid); - // Don't play Stand here — per-frame sync will do it when movement ceases. - return; - } - if (!renderer) return; auto* cr = renderer->getCharacterRenderer(); if (!cr) return; @@ -2810,6 +2803,19 @@ void Application::setupUICallbacks() { cr->playAnimation(instanceId, animId, /*loop=*/true); }); + // Unit move-flags callback — updates swimming and walking state from every MSG_MOVE_* packet. + // This is more reliable than opcode-based hints for cold joins and heartbeats: + // a player already swimming when we join will have SWIMMING set on the first heartbeat. + // Walking(4) vs Running(5) is also driven here from the WALKING flag. + gameHandler->setUnitMoveFlagsCallback([this](uint64_t guid, uint32_t moveFlags) { + const bool isSwimming = (moveFlags & static_cast(game::MovementFlags::SWIMMING)) != 0; + const bool isWalking = (moveFlags & static_cast(game::MovementFlags::WALKING)) != 0; + if (isSwimming) creatureSwimmingState_[guid] = true; + else creatureSwimmingState_.erase(guid); + if (isWalking) creatureWalkingState_[guid] = true; + else creatureWalkingState_.erase(guid); + }); + // Emote animation callback — play server-driven emote animations on NPCs and other players gameHandler->setEmoteAnimCallback([this](uint64_t guid, uint32_t emoteAnim) { if (!renderer || emoteAnim == 0) return; @@ -6927,6 +6933,7 @@ void Application::despawnOnlinePlayer(uint64_t guid) { onlinePlayerAppearance_.erase(guid); pendingOnlinePlayerEquipment_.erase(guid); creatureSwimmingState_.erase(guid); + creatureWalkingState_.erase(guid); } void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { @@ -8521,6 +8528,7 @@ void Application::despawnOnlineCreature(uint64_t guid) { creatureWeaponAttachAttempts_.erase(guid); creatureWasMoving_.erase(guid); creatureSwimmingState_.erase(guid); + creatureWalkingState_.erase(guid); LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 66a37b7b..8825cb25 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -12174,7 +12174,6 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_SWIM)) || (wireOp == wireOpcode(Opcode::MSG_MOVE_FALL_LAND)); const bool isJumpOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_JUMP)); - const bool isSwimOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_START_SWIM)); // For stop opcodes snap the entity position (duration=0) so it doesn't keep interpolating, // and pass durationMs=0 to the renderer so the Run-anim flash is suppressed. @@ -12189,13 +12188,16 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { } // Signal specific animation transitions that the per-frame sync can't detect reliably. - // WoW M2 animation IDs: 38=JumpMid (loops during airborne), 42=Swim - // animId=0 signals "exit swim mode" (MSG_MOVE_STOP_SWIM) so per-frame sync reverts to Stand. - const bool isStopSwimOpcode = (wireOp == wireOpcode(Opcode::MSG_MOVE_STOP_SWIM)); - if (unitAnimHintCallback_) { - if (isJumpOpcode) unitAnimHintCallback_(moverGuid, 38u); - else if (isSwimOpcode) unitAnimHintCallback_(moverGuid, 42u); - else if (isStopSwimOpcode) unitAnimHintCallback_(moverGuid, 0u); + // WoW M2 animation ID 38=JumpMid (loops during airborne). + // Swim/walking state is now authoritative from the movement flags field via unitMoveFlagsCallback_. + if (unitAnimHintCallback_ && isJumpOpcode) { + unitAnimHintCallback_(moverGuid, 38u); + } + + // Fire move-flags callback so application.cpp can update swimming/walking state + // from the flags field embedded in every movement packet (covers heartbeats and cold joins). + if (unitMoveFlagsCallback_) { + unitMoveFlagsCallback_(moverGuid, info.flags); } }