From f3f3b6288006cc92dcd36c61aa7fcc4a6259c17f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Feb 2026 00:54:38 -0800 Subject: [PATCH] Transport hell --- include/core/application.hpp | 3 +- include/game/game_handler.hpp | 21 +- include/game/opcodes.hpp | 1 + include/game/transport_manager.hpp | 54 ++- include/rendering/m2_renderer.hpp | 7 + include/rendering/wmo_renderer.hpp | 7 + src/core/application.cpp | 218 ++++++++-- src/game/game_handler.cpp | 56 ++- src/game/transport_manager.cpp | 611 +++++++++++++++++++++++++---- src/game/world_packets.cpp | 16 +- src/rendering/m2_renderer.cpp | 22 ++ src/rendering/water_renderer.cpp | 22 +- 12 files changed, 912 insertions(+), 126 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index f023c53f..4c8de8fd 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -87,7 +87,7 @@ private: void despawnOnlineCreature(uint64_t guid); void buildCreatureDisplayLookups(); std::string getModelPathForDisplayId(uint32_t displayId) const; - void spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation); + void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation); void despawnOnlineGameObject(uint64_t guid); void buildGameObjectDisplayLookups(); std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const; @@ -197,6 +197,7 @@ private: struct PendingGameObjectSpawn { uint64_t guid; + uint32_t entry; uint32_t displayId; float x, y, z, orientation; }; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 04ddb7de..57bfbe7f 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -462,8 +462,8 @@ public: void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); } // GameObject spawn callback (online mode - triggered when gameobject enters view) - // Parameters: guid, displayId, x, y, z (canonical), orientation - using GameObjectSpawnCallback = std::function; + // Parameters: guid, entry, displayId, x, y, z (canonical), orientation + using GameObjectSpawnCallback = std::function; void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); } // GameObject despawn callback (online mode - triggered when gameobject leaves view) @@ -483,10 +483,25 @@ public: using TransportMoveCallback = std::function; void setTransportMoveCallback(TransportMoveCallback cb) { transportMoveCallback_ = std::move(cb); } + // Transport spawn callback (online mode - triggered when transport GameObject is first detected) + // Parameters: guid, entry, displayId, x, y, z (canonical), orientation + using TransportSpawnCallback = std::function; + void setTransportSpawnCallback(TransportSpawnCallback cb) { transportSpawnCallback_ = std::move(cb); } + + // Notify that a transport has been spawned (called after WMO instance creation) + void notifyTransportSpawned(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { + if (transportSpawnCallback_) { + transportSpawnCallback_(guid, entry, displayId, x, y, z, orientation); + } + } + // Transport state for player-on-transport bool isOnTransport() const { return playerTransportGuid_ != 0; } uint64_t getPlayerTransportGuid() const { return playerTransportGuid_; } glm::vec3 getPlayerTransportOffset() const { return playerTransportOffset_; } + + // Check if a GUID is a known transport + bool isTransportGuid(uint64_t guid) const { return transportGuids_.count(guid) > 0; } glm::vec3 getComposedWorldPosition(); // Compose transport transform * local offset TransportManager* getTransportManager() { return transportManager_.get(); } void setPlayerOnTransport(uint64_t transportGuid, const glm::vec3& localOffset) { @@ -792,6 +807,7 @@ private: // ---- Creature movement handler ---- void handleMonsterMove(network::Packet& packet); + void handleMonsterMoveTransport(network::Packet& packet); // ---- Phase 5 handlers ---- void handleLootResponse(network::Packet& packet); @@ -979,6 +995,7 @@ private: CreatureDespawnCallback creatureDespawnCallback_; CreatureMoveCallback creatureMoveCallback_; TransportMoveCallback transportMoveCallback_; + TransportSpawnCallback transportSpawnCallback_; GameObjectSpawnCallback gameObjectSpawnCallback_; GameObjectDespawnCallback gameObjectDespawnCallback_; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index ba0e75cb..3cce13d6 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -50,6 +50,7 @@ enum class Opcode : uint16_t { // ---- Entity/Object updates ---- SMSG_UPDATE_OBJECT = 0x0A9, SMSG_COMPRESSED_UPDATE_OBJECT = 0x1F6, + SMSG_MONSTER_MOVE_TRANSPORT = 0x2AE, SMSG_DESTROY_OBJECT = 0x0AA, // ---- Chat ---- diff --git a/include/game/transport_manager.hpp b/include/game/transport_manager.hpp index b928866f..76f89e22 100644 --- a/include/game/transport_manager.hpp +++ b/include/game/transport_manager.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,22 +11,29 @@ namespace wowee::rendering { class WMORenderer; } +namespace wowee::pipeline { + class AssetManager; +} + namespace wowee::game { +struct TimedPoint { + uint32_t tMs; // Time in milliseconds from DBC + glm::vec3 pos; // Position at this time +}; + struct TransportPath { uint32_t pathId; - std::vector waypoints; // Position keyframes - std::vector rotations; // Optional rotation keyframes - bool looping; - float speed; // units/sec (default 18.0f like taxi) + std::vector points; // Time-indexed waypoints (includes duplicate first point at end for wrap) + bool looping; // Set to false after adding explicit wrap point + uint32_t durationMs; // Total loop duration in ms (includes wrap segment if added) }; struct ActiveTransport { uint64_t guid; // Entity GUID uint32_t wmoInstanceId; // WMO renderer instance ID uint32_t pathId; // Current path - size_t currentSegment; // Current waypoint index - float segmentProgress; // Distance along segment + glm::vec3 basePosition; // Spawn position (base offset for path) glm::vec3 position; // Current world position glm::quat rotation; // Current world rotation glm::mat4 transform; // Cached world transform @@ -39,6 +47,17 @@ struct ActiveTransport { glm::vec3 deckMin; glm::vec3 deckMax; bool hasDeckBounds; + + // Time-based animation (deterministic, no drift) + uint32_t localClockMs; // Local path time in milliseconds + bool hasServerClock; // Whether we've synced with server time + int32_t serverClockOffsetMs; // Offset: serverClock - localNow + bool useClientAnimation; // Use client-side path animation + float serverYaw; // Server-authoritative yaw (radians) + bool hasServerYaw; // Whether we've received server yaw + + float lastServerUpdate; // Time of last server movement update + int serverUpdateCount; // Number of server updates received }; class TransportManager { @@ -49,7 +68,7 @@ public: void setWMORenderer(rendering::WMORenderer* renderer) { wmoRenderer_ = renderer; } void update(float deltaTime); - void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId); + void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos); void unregisterTransport(uint64_t guid); ActiveTransport* getTransport(uint64_t guid); @@ -59,15 +78,30 @@ public: void loadPathFromNodes(uint32_t pathId, const std::vector& waypoints, bool looping = true, float speed = 18.0f); void setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max); + // Load transport paths from TransportAnimation.dbc + bool loadTransportAnimationDBC(pipeline::AssetManager* assetMgr); + + // Check if a path exists for a given GameObject entry + bool hasPathForEntry(uint32_t entry) const; + + // Update server-controlled transport position/rotation directly (bypasses path movement) + void updateServerTransport(uint64_t guid, const glm::vec3& position, float orientation); + + // Enable/disable client-side animation for transports without server updates + void setClientSideAnimation(bool enabled) { clientSideAnimation_ = enabled; } + bool isClientSideAnimation() const { return clientSideAnimation_; } + private: void updateTransportMovement(ActiveTransport& transport, float deltaTime); - glm::vec3 interpolatePath(const TransportPath& path, size_t segmentIdx, float t); - glm::quat calculateOrientation(const TransportPath& path, size_t segmentIdx, float t); + glm::vec3 evalTimedCatmullRom(const TransportPath& path, uint32_t pathTimeMs); + glm::quat orientationFromTangent(const TransportPath& path, uint32_t pathTimeMs); void updateTransformMatrices(ActiveTransport& transport); std::unordered_map transports_; - std::unordered_map paths_; + std::unordered_map paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc) rendering::WMORenderer* wmoRenderer_ = nullptr; + bool clientSideAnimation_ = true; // Enable client animation - smooth movement, synced with server updates + float elapsedTime_ = 0.0f; // Total elapsed time (seconds) }; } // namespace wowee::game diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 79459e04..296ce782 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -252,6 +252,13 @@ public: */ void setInstancePosition(uint32_t instanceId, const glm::vec3& position); + /** + * Update the full transform of an existing instance (e.g., for WMO doodads following parent WMO) + * @param instanceId Instance ID returned by createInstance() + * @param transform New world transform matrix + */ + void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform); + /** * Remove a specific instance by ID * @param instanceId Instance ID returned by createInstance() diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 8859655f..8b6b2885 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -406,6 +406,13 @@ private: glm::vec3 worldBoundsMax; std::vector> worldGroupBounds; + // Doodad tracking: M2 instances that are children of this WMO + struct DoodadInfo { + uint32_t m2InstanceId; // ID of the M2 instance + glm::mat4 localTransform; // Local transform relative to WMO origin + }; + std::vector doodads; + void updateModelMatrix(); }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 28fb777a..97cd3b19 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -129,6 +129,11 @@ bool Application::initialize() { LOG_INFO("Asset manager initialized successfully"); // Eagerly load creature display DBC lookups so first spawn doesn't stall buildCreatureDisplayLookups(); + + // Load transport paths from TransportAnimation.dbc + if (gameHandler && gameHandler->getTransportManager()) { + gameHandler->getTransportManager()->loadTransportAnimationDBC(assetManager.get()); + } } else { LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable"); LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory"); @@ -486,6 +491,14 @@ void Application::update(float deltaTime) { if (renderer && gameHandler) { bool onTransport = gameHandler->isOnTransport(); + // Debug: Log transport state changes + static bool wasOnTransport = false; + if (onTransport != wasOnTransport) { + LOG_INFO("Transport state changed: onTransport=", onTransport, + " guid=0x", std::hex, gameHandler->getPlayerTransportGuid(), std::dec); + wasOnTransport = onTransport; + } + if (onTaxi) { auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid()); if (playerEntity) { @@ -757,8 +770,8 @@ void Application::setupUICallbacks() { }); // GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.) - gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { - pendingGameObjectSpawns_.push_back({guid, displayId, x, y, z, orientation}); + gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { + pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation}); }); // GameObject despawn callback (online mode) - remove static models @@ -848,34 +861,134 @@ void Application::setupUICallbacks() { } }); - // Transport move callback (online mode) - update transport gameobject positions - gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float /*orientation*/) { + // Transport spawn callback (online mode) - register transports with TransportManager + gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { + auto* transportManager = gameHandler->getTransportManager(); + if (!transportManager || !renderer) return; + + // Get the WMO instance ID from the GameObject spawn auto it = gameObjectInstances_.find(guid); - if (it == gameObjectInstances_.end()) return; - glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); - if (renderer) { - if (it->second.isWmo) { - if (auto* wmoRenderer = renderer->getWMORenderer()) { - wmoRenderer->setInstancePosition(it->second.instanceId, renderPos); + if (it == gameObjectInstances_.end()) { + LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec); + return; + } + + uint32_t wmoInstanceId = it->second.instanceId; + LOG_INFO("Registering server transport: GUID=0x", std::hex, guid, std::dec, + " entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId, + " pos=(", x, ", ", y, ", ", z, ")"); + + // TransportAnimation.dbc is indexed by GameObject entry + uint32_t pathId = entry; + + bool clientAnim = transportManager->isClientSideAnimation(); + LOG_INFO("Transport spawn callback: clientAnimation=", clientAnim, + " guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId); + + // Coordinates are already canonical (converted in game_handler.cpp when entity was created) + glm::vec3 canonicalSpawnPos(x, y, z); + + // Check if we have a real path from TransportAnimation.dbc (indexed by entry) + if (!transportManager->hasPathForEntry(entry)) { + LOG_WARNING("No TransportAnimation.dbc path for entry ", entry, + " - transport will be stationary"); + + // Fallback: Stationary at spawn point (wait for server to send real position) + std::vector path = { canonicalSpawnPos }; + transportManager->loadPathFromNodes(pathId, path, false, 0.0f); + } else { + LOG_INFO("Using real transport path from TransportAnimation.dbc for entry ", entry); + } + + // Register the transport with spawn position (prevents rendering at origin until server update) + transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos); + + if (clientAnim) { + LOG_INFO("Transport registered - client-side animation enabled"); + } else { + // Only call updateServerTransport if client animation is disabled + // This sets the exact spawn position for server-controlled transports + // Coordinates are already canonical (converted in game_handler.cpp) + glm::vec3 canonicalPos(x, y, z); + transportManager->updateServerTransport(guid, canonicalPos, orientation); + LOG_INFO("Transport registered - server-controlled movement"); + } + }); + + // Transport move callback (online mode) - update transport gameobject positions + gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float orientation) { + LOG_INFO("Transport move callback: GUID=0x", std::hex, guid, std::dec, + " pos=(", x, ", ", y, ", ", z, ") orientation=", orientation); + + auto* transportManager = gameHandler->getTransportManager(); + if (!transportManager) { + LOG_WARNING("Transport move callback: TransportManager is null!"); + return; + } + + // Check if transport exists - if not, treat this as a late spawn (reconnection/server restart) + if (!transportManager->getTransport(guid)) { + LOG_INFO("Received position update for unregistered transport 0x", std::hex, guid, std::dec, + " - auto-spawning from position update"); + + // Get transport info from entity manager + auto entity = gameHandler->getEntityManager().getEntity(guid); + if (entity && entity->getType() == game::ObjectType::GAMEOBJECT) { + auto go = std::static_pointer_cast(entity); + uint32_t entry = go->getEntry(); + uint32_t displayId = go->getDisplayId(); + + // Find the WMO instance for this transport (should exist from earlier GameObject spawn) + auto it = gameObjectInstances_.find(guid); + if (it != gameObjectInstances_.end()) { + uint32_t wmoInstanceId = it->second.instanceId; + + // TransportAnimation.dbc is indexed by GameObject entry + uint32_t pathId = entry; + + // Coordinates are already canonical (converted in game_handler.cpp) + glm::vec3 canonicalSpawnPos(x, y, z); + + // Check if we have a real path, otherwise create stationary fallback + if (!transportManager->hasPathForEntry(entry)) { + std::vector path = { canonicalSpawnPos }; + transportManager->loadPathFromNodes(pathId, path, false, 0.0f); + LOG_INFO("Auto-spawned transport with stationary path: entry=", entry, + " displayId=", displayId, " wmoInstance=", wmoInstanceId); + } else { + LOG_INFO("Auto-spawned transport with real path: entry=", entry, + " displayId=", displayId, " wmoInstance=", wmoInstanceId); + } + + transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos); + } else { + LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, + " - WMO instance not found"); + return; } } else { - if (auto* m2Renderer = renderer->getM2Renderer()) { - m2Renderer->setInstancePosition(it->second.instanceId, renderPos); - } + LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, + " - entity not found in EntityManager"); + return; } + } - // Move player with transport if riding it - if (gameHandler && gameHandler->isOnTransport() && gameHandler->getPlayerTransportGuid() == guid) { - auto* cc = renderer->getCameraController(); - if (cc) { - glm::vec3* ft = cc->getFollowTargetMutable(); - if (ft) { - // Transport offset is in server/canonical coords — convert to render - glm::vec3 offset = gameHandler->getPlayerTransportOffset(); - glm::vec3 canonicalPlayerPos = glm::vec3(x + offset.x, y + offset.y, z + offset.z); - glm::vec3 playerRenderPos = core::coords::canonicalToRender(canonicalPlayerPos); - *ft = playerRenderPos; - } + // Update TransportManager's internal state (position, rotation, transform matrices) + // This also updates the WMO renderer automatically + // Coordinates are already canonical (converted in game_handler.cpp when entity was created) + glm::vec3 canonicalPos(x, y, z); + transportManager->updateServerTransport(guid, canonicalPos, orientation); + + // Move player with transport if riding it + if (gameHandler && gameHandler->isOnTransport() && gameHandler->getPlayerTransportGuid() == guid && renderer) { + auto* cc = renderer->getCameraController(); + if (cc) { + glm::vec3* ft = cc->getFollowTargetMutable(); + if (ft) { + // Get player world position from TransportManager (handles transform properly) + glm::vec3 offset = gameHandler->getPlayerTransportOffset(); + glm::vec3 worldPos = transportManager->getPlayerWorldPosition(guid, offset); + *ft = worldPos; } } } @@ -1673,6 +1786,12 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float renderer->getTerrainManager()->setMapName(mapName); } + // Connect TransportManager to WMORenderer (for server transports) + if (gameHandler && gameHandler->getTransportManager() && renderer->getWMORenderer()) { + gameHandler->getTransportManager()->setWMORenderer(renderer->getWMORenderer()); + LOG_INFO("TransportManager connected to WMORenderer for online mode"); + } + showProgress("Loading character model...", 0.05f); // Build faction hostility map for this character's race @@ -2599,7 +2718,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); } -void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { +void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { if (!renderer || !assetManager) return; if (!gameObjectLookupsBuilt_) { @@ -2625,7 +2744,36 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float return; } - std::string modelPath = getGameObjectModelPathForDisplayId(displayId); + std::string modelPath; + + // Override model path for transports with wrong displayIds (preloaded transports) + // Check if this GUID is a known transport + bool isTransport = gameHandler && gameHandler->isTransportGuid(guid); + if (isTransport) { + // Map common transport displayIds to correct WMO paths + // DisplayIds 455, 462 = Elevators/Ships → try standard ship + // DisplayIds 807, 808 = Zeppelins + // DisplayIds 2454, 1587 = Special ships/icebreakers + if (displayId == 455 || displayId == 462 || displayId == 20808 || displayId == 176231 || displayId == 176310) { + modelPath = "World\\wmo\\transports\\transport_ship\\transportship.wmo"; + LOG_INFO("Overriding transport displayId ", displayId, " → transportship.wmo"); + } else if (displayId == 807 || displayId == 808 || displayId == 175080 || displayId == 176495 || displayId == 164871) { + modelPath = "World\\wmo\\transports\\transport_zeppelin\\transport_zeppelin.wmo"; + LOG_INFO("Overriding transport displayId ", displayId, " → transport_zeppelin.wmo"); + } else if (displayId == 1587) { + modelPath = "World\\wmo\\transports\\transport_horde_zeppelin\\Transport_Horde_Zeppelin.wmo"; + LOG_INFO("Overriding transport displayId ", displayId, " → Transport_Horde_Zeppelin.wmo"); + } else if (displayId == 2454 || displayId == 181688 || displayId == 190536) { + modelPath = "World\\wmo\\transports\\icebreaker\\Transport_Icebreaker_ship.wmo"; + LOG_INFO("Overriding transport displayId ", displayId, " → Transport_Icebreaker_ship.wmo"); + } + } + + // Fallback to normal displayId lookup if not a transport or no override matched + if (modelPath.empty()) { + modelPath = getGameObjectModelPathForDisplayId(displayId); + } + if (modelPath.empty()) { LOG_WARNING("No model path for gameobject displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")"); return; @@ -2721,6 +2869,18 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float gameObjectInstances_[guid] = {modelId, instanceId, true}; LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec, " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); + + // Check if this is a transport and notify via special method + if (gameHandler) { + std::string lowerModelPath = modelPath; + std::transform(lowerModelPath.begin(), lowerModelPath.end(), lowerModelPath.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lowerModelPath.find("transport") != std::string::npos) { + // This is a transport GameObject - notify the game handler + gameHandler->notifyTransportSpawned(guid, entry, displayId, x, y, z, orientation); + } + } + return; } @@ -2798,7 +2958,7 @@ void Application::processGameObjectSpawnQueue() { int spawned = 0; while (!pendingGameObjectSpawns_.empty() && spawned < MAX_SPAWNS_PER_FRAME) { auto& s = pendingGameObjectSpawns_.front(); - spawnOnlineGameObject(s.guid, s.displayId, s.x, s.y, s.z, s.orientation); + spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation); pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin()); spawned++; } @@ -3214,7 +3374,7 @@ void Application::setupTestTransport() { // Register transport with transport manager uint64_t transportGuid = 0x1000000000000001ULL; // Fake GUID for test - transportManager->registerTransport(transportGuid, wmoInstanceId, pathId); + transportManager->registerTransport(transportGuid, wmoInstanceId, pathId, startCanonical); // Optional: Set deck bounds (rough estimate for a ship deck) transportManager->setDeckBounds(transportGuid, diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 99e05644..b3ff6395 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -601,6 +601,10 @@ void GameHandler::handlePacket(network::Packet& packet) { handleMonsterMove(packet); break; + case Opcode::SMSG_MONSTER_MOVE_TRANSPORT: + handleMonsterMoveTransport(packet); + break; + // ---- Speed Changes ---- case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE: handleForceRunSpeedChange(packet); @@ -2007,9 +2011,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec, " displayId=", go->getDisplayId(), " pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")"); + // Note: TransportSpawnCallback will be invoked from Application after WMO instance is created } if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) { - gameObjectSpawnCallback_(block.guid, go->getDisplayId(), + gameObjectSpawnCallback_(block.guid, go->getEntry(), go->getDisplayId(), go->getX(), go->getY(), go->getZ(), go->getOrientation()); } // Fire transport move callback for transports (position update on re-creation) @@ -2337,6 +2342,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } case UpdateType::MOVEMENT: { + // Diagnostic: Log if we receive MOVEMENT blocks for transports + if (transportGuids_.count(block.guid)) { + LOG_INFO("MOVEMENT update for transport 0x", std::hex, block.guid, std::dec, + " pos=(", block.x, ", ", block.y, ", ", block.z, ")"); + } + // Update entity position (server → canonical) auto entity = entityManager.getEntity(block.guid); if (entity) { @@ -4247,6 +4258,49 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { } } +void GameHandler::handleMonsterMoveTransport(network::Packet& packet) { + // Parse transport-relative creature movement (NPCs on boats/zeppelins) + // Packet structure: mover GUID + transport GUID + spline data (local coords) + + uint64_t moverGuid = packet.readUInt64(); + uint8_t unk = packet.readUInt8(); // Unknown byte (usually 0) + uint64_t transportGuid = packet.readUInt64(); + + // Transport-local coordinates + float localX = packet.readFloat(); + float localY = packet.readFloat(); + float localZ = packet.readFloat(); + + LOG_INFO("SMSG_MONSTER_MOVE_TRANSPORT: mover=0x", std::hex, moverGuid, + " transport=0x", transportGuid, std::dec, + " localPos=(", localX, ", ", localY, ", ", localZ, ")"); + + // Compose world position: worldPos = transportTransform * localPos + auto entity = entityManager.getEntity(moverGuid); + if (!entity) { + LOG_WARNING(" NPC 0x", std::hex, moverGuid, std::dec, " not found in entity manager"); + return; + } + + if (transportManager_) { + // Use TransportManager to compose world position from local offset + glm::vec3 localPos(localX, localY, localZ); + glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localPos); + + entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation()); + + LOG_INFO(" Composed NPC world position: (", worldPos.x, ", ", worldPos.y, ", ", worldPos.z, ")"); + } else { + LOG_WARNING(" TransportManager not available for NPC position composition"); + } + + // TODO: Parse full spline data for smooth NPC movement on transport + // Then update entity position and call creatureMoveCallback_ + + // Suppress unused variable warning for now + (void)unk; +} + void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { AttackerStateUpdateData data; if (!AttackerStateUpdateParser::parse(packet, data)) return; diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 2f2d9d2e..d1526bb7 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -1,9 +1,15 @@ #include "game/transport_manager.hpp" #include "rendering/wmo_renderer.hpp" +#include "core/coordinates.hpp" +#include "core/logger.hpp" +#include "pipeline/dbc_loader.hpp" +#include "pipeline/asset_manager.hpp" #include #include #include #include +#include +#include namespace wowee::game { @@ -11,12 +17,16 @@ TransportManager::TransportManager() = default; TransportManager::~TransportManager() = default; void TransportManager::update(float deltaTime) { + elapsedTime_ += deltaTime; + for (auto& [guid, transport] : transports_) { + // Once we have server clock offset, we can predict server time indefinitely + // No need for watchdog - keep using the offset even if server updates stop updateTransportMovement(transport, deltaTime); } } -void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId) { +void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos) { auto pathIt = paths_.find(pathId); if (pathIt == paths_.end()) { std::cerr << "TransportManager: Path " << pathId << " not found for transport " << guid << std::endl; @@ -24,7 +34,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, } const auto& path = pathIt->second; - if (path.waypoints.empty()) { + if (path.points.empty()) { std::cerr << "TransportManager: Path " << pathId << " has no waypoints" << std::endl; return; } @@ -33,20 +43,49 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, transport.guid = guid; transport.wmoInstanceId = wmoInstanceId; transport.pathId = pathId; - transport.currentSegment = 0; - transport.segmentProgress = 0.0f; - transport.position = path.waypoints[0]; + + // CRITICAL: Set basePosition from spawn position and t=0 offset + // For stationary paths (1 waypoint), just use spawn position directly + if (path.durationMs == 0 || path.points.size() <= 1) { + // Stationary transport - no path animation + transport.basePosition = spawnWorldPos; + transport.position = spawnWorldPos; + } else { + // Moving transport - infer base from first path offset + glm::vec3 offset0 = evalTimedCatmullRom(path, 0); + transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn + transport.position = spawnWorldPos; // Start at spawn position (base + offset0) + } + transport.rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion transport.playerOnBoard = false; transport.playerLocalOffset = glm::vec3(0.0f); transport.hasDeckBounds = false; + transport.localClockMs = 0; + transport.hasServerClock = false; + transport.serverClockOffsetMs = 0; + transport.useClientAnimation = clientSideAnimation_; // Enable client animation by default + transport.serverYaw = 0.0f; + transport.hasServerYaw = false; + transport.lastServerUpdate = 0.0f; + transport.serverUpdateCount = 0; updateTransformMatrices(transport); + // CRITICAL: Update WMO renderer with initial transform + if (wmoRenderer_) { + wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + } + transports_[guid] = transport; - std::cout << "TransportManager: Registered transport " << guid - << " at path " << pathId << " with " << path.waypoints.size() << " waypoints" << std::endl; + glm::vec3 renderPos = core::coords::canonicalToRender(transport.position); + LOG_INFO("TransportManager: Registered transport 0x", std::hex, guid, std::dec, + " at path ", pathId, " with ", path.points.size(), " waypoints", + " wmoInstanceId=", wmoInstanceId, + " spawnPos=(", spawnWorldPos.x, ", ", spawnWorldPos.y, ", ", spawnWorldPos.z, ")", + " basePos=(", transport.basePosition.x, ", ", transport.basePosition.y, ", ", transport.basePosition.z, ")", + " initialRenderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")"); } void TransportManager::unregisterTransport(uint64_t guid) { @@ -89,15 +128,33 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector 0.0f ? (uint32_t)((dist / speed) * 1000.0f) : 1000; + cumulativeMs += segmentMs; + path.points.push_back({cumulativeMs, waypoints[i]}); + } + path.durationMs = cumulativeMs; + } paths_[pathId] = path; - std::cout << "TransportManager: Loaded path " << pathId - << " with " << waypoints.size() << " waypoints, " - << "looping=" << looping << ", speed=" << speed << std::endl; + LOG_INFO("TransportManager: Loaded path ", pathId, + " with ", waypoints.size(), " waypoints, " + "looping=", looping, ", speed=", speed); } void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) { @@ -119,60 +176,54 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float } const auto& path = pathIt->second; - if (path.waypoints.size() < 2) { - return; // Need at least 2 waypoints to move - } - - // Calculate segment length - glm::vec3 p0 = path.waypoints[transport.currentSegment]; - size_t nextIdx = (transport.currentSegment + 1) % path.waypoints.size(); - glm::vec3 p1 = path.waypoints[nextIdx]; - float segmentLength = glm::distance(p0, p1); - - if (segmentLength < 0.001f) { - // Zero-length segment, skip to next - transport.currentSegment = nextIdx; - transport.segmentProgress = 0.0f; + if (path.points.empty()) { return; } - // Update progress - float distanceThisFrame = path.speed * deltaTime; - transport.segmentProgress += distanceThisFrame; - - // Check if we've completed this segment - while (transport.segmentProgress >= segmentLength) { - transport.segmentProgress -= segmentLength; - transport.currentSegment = nextIdx; - - // Check for path completion - if (!path.looping && transport.currentSegment >= path.waypoints.size() - 1) { - // Reached end of non-looping path - transport.currentSegment = path.waypoints.size() - 1; - transport.segmentProgress = 0.0f; - transport.position = path.waypoints[transport.currentSegment]; - updateTransformMatrices(transport); - return; - } - - // Update for next segment - p0 = path.waypoints[transport.currentSegment]; - nextIdx = (transport.currentSegment + 1) % path.waypoints.size(); - p1 = path.waypoints[nextIdx]; - segmentLength = glm::distance(p0, p1); - - if (segmentLength < 0.001f) { - transport.segmentProgress = 0.0f; - continue; + // Stationary transport (durationMs = 0) + if (path.durationMs == 0) { + // Just update transform (position already set) + updateTransformMatrices(transport); + if (wmoRenderer_) { + wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); } + return; } - // Interpolate position - float t = transport.segmentProgress / segmentLength; - transport.position = interpolatePath(path, transport.currentSegment, t); + // Evaluate path time + uint32_t nowMs = (uint32_t)(elapsedTime_ * 1000.0f); + uint32_t pathTimeMs = 0; - // Calculate orientation from path tangent - transport.rotation = calculateOrientation(path, transport.currentSegment, t); + if (transport.hasServerClock) { + // Predict server time using clock offset (works for both client and server-driven modes) + int64_t serverTimeMs = (int64_t)nowMs + transport.serverClockOffsetMs; + int64_t mod = (int64_t)path.durationMs; + int64_t wrapped = serverTimeMs % mod; + if (wrapped < 0) wrapped += mod; + pathTimeMs = (uint32_t)wrapped; + } else if (transport.useClientAnimation) { + // Pure local clock (no server sync yet, client-driven) + transport.localClockMs += (uint32_t)(deltaTime * 1000.0f); + pathTimeMs = transport.localClockMs % path.durationMs; + } else { + // Server-driven but no clock yet - don't move + updateTransformMatrices(transport); + if (wmoRenderer_) { + wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + } + return; + } + + // Evaluate position from time (path is local offsets, add base position) + glm::vec3 pathOffset = evalTimedCatmullRom(path, pathTimeMs); + transport.position = transport.basePosition + pathOffset; + + // Use server yaw if available (authoritative), otherwise compute from tangent + if (transport.hasServerYaw) { + transport.rotation = glm::angleAxis(transport.serverYaw, glm::vec3(0.0f, 0.0f, 1.0f)); + } else { + transport.rotation = orientationFromTangent(path, pathTimeMs); + } // Update transform matrices updateTransformMatrices(transport); @@ -181,11 +232,51 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float if (wmoRenderer_) { wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); } + + // Debug logging every 120 frames (~2 seconds at 60fps) + static int debugFrameCount = 0; + if (debugFrameCount++ % 120 == 0) { + // Log canonical position AND render position to check coordinate conversion + glm::vec3 renderPos = core::coords::canonicalToRender(transport.position); + LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec, + " pathTime=", pathTimeMs, "ms / ", path.durationMs, "ms", + " canonicalPos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")", + " renderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")", + " basePos=(", transport.basePosition.x, ", ", transport.basePosition.y, ", ", transport.basePosition.z, ")", + " pathOffset=(", pathOffset.x, ", ", pathOffset.y, ", ", pathOffset.z, ")", + " mode=", (transport.useClientAnimation ? "client" : "server"), + " hasServerClock=", transport.hasServerClock, + " offset=", transport.serverClockOffsetMs, "ms"); + } } -glm::vec3 TransportManager::interpolatePath(const TransportPath& path, size_t segmentIdx, float t) { - // Catmull-Rom spline interpolation (same as taxi flights) - size_t numPoints = path.waypoints.size(); +glm::vec3 TransportManager::evalTimedCatmullRom(const TransportPath& path, uint32_t pathTimeMs) { + if (path.points.empty()) { + return glm::vec3(0.0f); + } + if (path.points.size() == 1) { + return path.points[0].pos; + } + + // Find the segment containing pathTimeMs + size_t segmentIdx = 0; + bool found = false; + + for (size_t i = 0; i + 1 < path.points.size(); i++) { + if (pathTimeMs >= path.points[i].tMs && pathTimeMs < path.points[i + 1].tMs) { + segmentIdx = i; + found = true; + break; + } + } + + // Handle not found (wraparound or timing gaps) + if (!found) { + segmentIdx = path.looping ? (path.points.size() - 1) : + (path.points.size() >= 2 ? path.points.size() - 2 : 0); + } + + size_t numPoints = path.points.size(); // Get 4 control points for Catmull-Rom size_t p0Idx = (segmentIdx == 0) ? (path.looping ? numPoints - 1 : 0) : segmentIdx - 1; @@ -193,17 +284,24 @@ glm::vec3 TransportManager::interpolatePath(const TransportPath& path, size_t se size_t p2Idx = (segmentIdx + 1) % numPoints; size_t p3Idx = (segmentIdx + 2) % numPoints; - // If non-looping and at boundaries, clamp indices if (!path.looping) { if (segmentIdx == 0) p0Idx = 0; if (segmentIdx >= numPoints - 2) p3Idx = numPoints - 1; if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1; } - glm::vec3 p0 = path.waypoints[p0Idx]; - glm::vec3 p1 = path.waypoints[p1Idx]; - glm::vec3 p2 = path.waypoints[p2Idx]; - glm::vec3 p3 = path.waypoints[p3Idx]; + glm::vec3 p0 = path.points[p0Idx].pos; + glm::vec3 p1 = path.points[p1Idx].pos; + glm::vec3 p2 = path.points[p2Idx].pos; + glm::vec3 p3 = path.points[p3Idx].pos; + + // Calculate t (0.0 to 1.0 within segment) + // No special case needed - wrap point is explicit in the array now + uint32_t t1Ms = path.points[p1Idx].tMs; + uint32_t t2Ms = path.points[p2Idx].tMs; + uint32_t segmentDurationMs = (t2Ms > t1Ms) ? (t2Ms - t1Ms) : 1; + float t = (float)(pathTimeMs - t1Ms) / (float)segmentDurationMs; + t = glm::clamp(t, 0.0f, 1.0f); // Catmull-Rom spline formula float t2 = t * t; @@ -219,9 +317,33 @@ glm::vec3 TransportManager::interpolatePath(const TransportPath& path, size_t se return result; } -glm::quat TransportManager::calculateOrientation(const TransportPath& path, size_t segmentIdx, float t) { - // Calculate tangent vector for orientation - size_t numPoints = path.waypoints.size(); +glm::quat TransportManager::orientationFromTangent(const TransportPath& path, uint32_t pathTimeMs) { + if (path.points.empty()) { + return glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + } + if (path.points.size() == 1) { + return glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + } + + // Find the segment containing pathTimeMs + size_t segmentIdx = 0; + bool found = false; + + for (size_t i = 0; i + 1 < path.points.size(); i++) { + if (pathTimeMs >= path.points[i].tMs && pathTimeMs < path.points[i + 1].tMs) { + segmentIdx = i; + found = true; + break; + } + } + + // Handle not found (wraparound or timing gaps) + if (!found) { + segmentIdx = path.looping ? (path.points.size() - 1) : + (path.points.size() >= 2 ? path.points.size() - 2 : 0); + } + + size_t numPoints = path.points.size(); // Get 4 control points size_t p0Idx = (segmentIdx == 0) ? (path.looping ? numPoints - 1 : 0) : segmentIdx - 1; @@ -235,10 +357,18 @@ glm::quat TransportManager::calculateOrientation(const TransportPath& path, size if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1; } - glm::vec3 p0 = path.waypoints[p0Idx]; - glm::vec3 p1 = path.waypoints[p1Idx]; - glm::vec3 p2 = path.waypoints[p2Idx]; - glm::vec3 p3 = path.waypoints[p3Idx]; + glm::vec3 p0 = path.points[p0Idx].pos; + glm::vec3 p1 = path.points[p1Idx].pos; + glm::vec3 p2 = path.points[p2Idx].pos; + glm::vec3 p3 = path.points[p3Idx].pos; + + // Calculate t (0.0 to 1.0 within segment) + // No special case needed - wrap point is explicit in the array now + uint32_t t1Ms = path.points[p1Idx].tMs; + uint32_t t2Ms = path.points[p2Idx].tMs; + uint32_t segmentDurationMs = (t2Ms > t1Ms) ? (t2Ms - t1Ms) : 1; + float t = (float)(pathTimeMs - t1Ms) / (float)segmentDurationMs; + t = glm::clamp(t, 0.0f, 1.0f); // Tangent of Catmull-Rom spline (derivative) float t2 = t * t; @@ -263,7 +393,6 @@ glm::quat TransportManager::calculateOrientation(const TransportPath& path, size tangent /= tangentLength; // Calculate rotation from forward direction - // WoW forward is typically +Y, but we'll use the tangent as forward glm::vec3 forward = tangent; glm::vec3 up(0.0f, 0.0f, 1.0f); // WoW Z is up @@ -285,13 +414,341 @@ glm::quat TransportManager::calculateOrientation(const TransportPath& path, size } void TransportManager::updateTransformMatrices(ActiveTransport& transport) { + // Convert position from canonical to render coordinates for WMO rendering + // Canonical: +X=North, +Y=West, +Z=Up + // Render: renderX=wowY (west), renderY=wowX (north), renderZ=wowZ (up) + glm::vec3 renderPos = core::coords::canonicalToRender(transport.position); + + // Convert rotation from canonical to render space using proper basis change + // Canonical → Render is a 90° CCW rotation around Z (swaps X and Y) + // Proper formula: q_render = q_basis * q_canonical * q_basis^-1 + glm::quat basisRotation = glm::angleAxis(glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + glm::quat basisInverse = glm::conjugate(basisRotation); + glm::quat renderRot = basisRotation * transport.rotation * basisInverse; + // Build transform matrix: translate * rotate * scale - glm::mat4 translation = glm::translate(glm::mat4(1.0f), transport.position); - glm::mat4 rotation = glm::mat4_cast(transport.rotation); + glm::mat4 translation = glm::translate(glm::mat4(1.0f), renderPos); + glm::mat4 rotation = glm::mat4_cast(renderRot); glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(1.0f)); // No scaling for transports transport.transform = translation * rotation * scale; transport.invTransform = glm::inverse(transport.transform); } +void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& position, float orientation) { + auto* transport = getTransport(guid); + if (!transport) { + LOG_WARNING("TransportManager::updateServerTransport: Transport not found: 0x", std::hex, guid, std::dec); + return; + } + + // Track server updates + transport->serverUpdateCount++; + transport->lastServerUpdate = elapsedTime_; + + auto pathIt = paths_.find(transport->pathId); + if (pathIt == paths_.end() || pathIt->second.durationMs == 0) { + // No path or stationary - just set position directly + transport->basePosition = position; + transport->position = position; + transport->rotation = glm::angleAxis(orientation, glm::vec3(0.0f, 0.0f, 1.0f)); + updateTransformMatrices(*transport); + if (wmoRenderer_) { + wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); + } + return; + } + + const auto& path = pathIt->second; + + // Seed basePosition from t=0 assumption before first search + // (t=0 corresponds to spawn point / first path point) + if (!transport->hasServerClock) { + glm::vec3 offset0 = evalTimedCatmullRom(path, 0); + transport->basePosition = position - offset0; + } + + // Estimate server's path time by projecting position onto path + // Path positions are local offsets, server position is world position + // basePosition = serverWorldPos - pathLocalOffset + + uint32_t bestTimeMs = 0; + float bestD2 = FLT_MAX; + glm::vec3 bestPathOffset(0.0f); + + // After initial sync, search only in small window around predicted time + bool hasInitialSync = transport->hasServerClock; + uint32_t nowMs = (uint32_t)(elapsedTime_ * 1000.0f); + uint32_t predictedTimeMs = 0; + if (hasInitialSync) { + // Predict where server should be based on last clock offset + int64_t serverTimeMs = (int64_t)nowMs + transport->serverClockOffsetMs; + int64_t mod = (int64_t)path.durationMs; + int64_t wrapped = serverTimeMs % mod; + if (wrapped < 0) wrapped += mod; + predictedTimeMs = (uint32_t)wrapped; + } + + uint32_t searchStart = 0; + uint32_t searchEnd = path.durationMs; + uint32_t sampleCount = 1000; // Dense sampling for accuracy + + if (hasInitialSync) { + // Search in ±5 second window around predicted time + uint32_t windowMs = 5000; + searchStart = (predictedTimeMs > windowMs) ? (predictedTimeMs - windowMs) : 0; + searchEnd = glm::min(predictedTimeMs + windowMs, path.durationMs); + sampleCount = 200; // Fewer samples needed in small window + } + + for (uint32_t i = 0; i < sampleCount; i++) { + // Map i to [searchStart, searchEnd) + uint32_t testTimeMs = searchStart + (uint32_t)((uint64_t)i * (searchEnd - searchStart) / sampleCount); + glm::vec3 testPathOffset = evalTimedCatmullRom(path, testTimeMs); + glm::vec3 testWorldPos = transport->basePosition + testPathOffset; // Convert local → world + glm::vec3 diff = testWorldPos - position; + float d2 = glm::dot(diff, diff); // distance² (cheaper, no sqrt) + if (d2 < bestD2) { + bestD2 = d2; + bestTimeMs = testTimeMs; + bestPathOffset = testPathOffset; + } + } + + // Refine with finer sampling around best match + uint32_t refineSampleCount = 50; + uint32_t refineWindow = glm::max(1u, (searchEnd - searchStart) / sampleCount); // Clamp to prevent zero + uint32_t refineStart = (bestTimeMs > refineWindow) ? (bestTimeMs - refineWindow) : 0; + uint32_t refineEnd = glm::min(bestTimeMs + refineWindow, path.durationMs); + uint32_t refineInterval = (refineEnd > refineStart) ? ((refineEnd - refineStart) / refineSampleCount) : 1; + if (refineInterval > 0) { + for (uint32_t i = 0; i < refineSampleCount; i++) { + uint32_t testTimeMs = refineStart + i * refineInterval; + glm::vec3 testPathOffset = evalTimedCatmullRom(path, testTimeMs); // local offset + glm::vec3 testWorldPos = transport->basePosition + testPathOffset; // Convert local → world + glm::vec3 diff = testWorldPos - position; // Compare world to world + float d2 = glm::dot(diff, diff); + if (d2 < bestD2) { + bestD2 = d2; + bestTimeMs = testTimeMs; + bestPathOffset = testPathOffset; // Update best offset when improving match + } + } + } + + float bestDistance = std::sqrt(bestD2); + + // Infer base position: serverWorldPos = basePos + pathOffset + // So: basePos = serverWorldPos - pathOffset + glm::vec3 inferredBasePos = position - bestPathOffset; + + // Compute server clock offset with wrap-aware smoothing + int32_t newOffset = (int32_t)bestTimeMs - (int32_t)nowMs; + + if (!transport->hasServerClock) { + // First sync: accept immediately and set base position + transport->basePosition = inferredBasePos; + transport->serverClockOffsetMs = newOffset; + transport->hasServerClock = true; + LOG_INFO("TransportManager: Initial server clock sync for transport 0x", std::hex, guid, std::dec, + " serverTime=", bestTimeMs, "ms / ", path.durationMs, "ms", + " drift=", bestDistance, " units", + " basePos=(", inferredBasePos.x, ", ", inferredBasePos.y, ", ", inferredBasePos.z, ")", + " offset=", newOffset, "ms"); + } else { + // Subsequent syncs: wrap-aware smoothing to avoid phase jumps + int32_t oldOffset = transport->serverClockOffsetMs; + int32_t delta = newOffset - oldOffset; + int32_t mod = (int32_t)path.durationMs; + + // Wrap delta to shortest path: [-mod/2, mod/2] + if (delta > mod / 2) delta -= mod; + if (delta < -mod / 2) delta += mod; + + // Smooth delta, not absolute offset + transport->serverClockOffsetMs = oldOffset + (int32_t)(0.1f * delta); + + // Only update basePosition if projection is accurate (< 5 units drift) + // This prevents "swim" from projection noise near ambiguous geometry + if (bestDistance < 5.0f) { + transport->basePosition = glm::mix(transport->basePosition, inferredBasePos, 0.1f); + LOG_INFO("TransportManager: Server clock correction for transport 0x", std::hex, guid, std::dec, + " drift=", bestDistance, " units (updated base)", + " oldOffset=", oldOffset, "ms → newOffset=", transport->serverClockOffsetMs, "ms", + " (delta=", delta, "ms, smoothed by 0.1)"); + } else { + LOG_INFO("TransportManager: Server clock correction for transport 0x", std::hex, guid, std::dec, + " drift=", bestDistance, " units (base unchanged, clock only)", + " oldOffset=", oldOffset, "ms → newOffset=", transport->serverClockOffsetMs, "ms", + " (delta=", delta, "ms, smoothed by 0.1)"); + } + } + + // Update position immediately from synced clock + glm::vec3 pathOffset = evalTimedCatmullRom(path, bestTimeMs); + transport->position = transport->basePosition + pathOffset; + + // Store server's authoritative yaw (orientation is in radians around Z axis) + transport->serverYaw = orientation; + transport->hasServerYaw = true; + transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f)); + + updateTransformMatrices(*transport); + if (wmoRenderer_) { + wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); + } +} + +bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) { + LOG_INFO("Loading TransportAnimation.dbc..."); + + if (!assetMgr) { + LOG_ERROR("AssetManager is null"); + return false; + } + + // Load DBC file + auto dbcData = assetMgr->readFile("DBFilesClient\\TransportAnimation.dbc"); + if (dbcData.empty()) { + LOG_WARNING("TransportAnimation.dbc not found - transports will use fallback paths"); + return false; + } + + pipeline::DBCFile dbc; + if (!dbc.load(dbcData)) { + LOG_ERROR("Failed to parse TransportAnimation.dbc"); + return false; + } + + LOG_INFO("TransportAnimation.dbc: ", dbc.getRecordCount(), " records, ", + dbc.getFieldCount(), " fields per record"); + + // Debug: dump first 3 records to see all field values + for (uint32_t i = 0; i < std::min(3u, dbc.getRecordCount()); i++) { + LOG_INFO(" DEBUG Record ", i, ": ", + " [0]=", dbc.getUInt32(i, 0), + " [1]=", dbc.getUInt32(i, 1), + " [2]=", dbc.getUInt32(i, 2), + " [3]=", dbc.getFloat(i, 3), + " [4]=", dbc.getFloat(i, 4), + " [5]=", dbc.getFloat(i, 5), + " [6]=", dbc.getUInt32(i, 6)); + } + + // Group waypoints by transportEntry + std::map>> waypointsByTransport; + + for (uint32_t i = 0; i < dbc.getRecordCount(); i++) { + // uint32_t id = dbc.getUInt32(i, 0); // Not needed + uint32_t transportEntry = dbc.getUInt32(i, 1); + uint32_t timeIndex = dbc.getUInt32(i, 2); + float posX = dbc.getFloat(i, 3); + float posY = dbc.getFloat(i, 4); + float posZ = dbc.getFloat(i, 5); + // uint32_t sequenceId = dbc.getUInt32(i, 6); // Not needed for basic paths + + // RAW FLOAT SANITY CHECK: Log first 10 records to see if DBC has real data + if (i < 10) { + uint32_t ux = dbc.getUInt32(i, 3); + uint32_t uy = dbc.getUInt32(i, 4); + uint32_t uz = dbc.getUInt32(i, 5); + LOG_INFO("TA raw rec ", i, + " entry=", transportEntry, + " t=", timeIndex, + " raw=(", posX, ",", posY, ",", posZ, ")", + " u32=(", ux, ",", uy, ",", uz, ")"); + } + + waypointsByTransport[transportEntry].push_back({timeIndex, glm::vec3(posX, posY, posZ)}); + } + + // Create time-indexed paths from waypoints + int pathsLoaded = 0; + for (const auto& [transportEntry, waypoints] : waypointsByTransport) { + if (waypoints.empty()) continue; + + // Sort by timeIndex + auto sortedWaypoints = waypoints; + std::sort(sortedWaypoints.begin(), sortedWaypoints.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + + // CRITICAL: Normalize timeIndex to start at 0 (DBC records don't start at 0!) + // This makes evalTimedCatmullRom(path, 0) valid and stabilizes basePosition seeding + uint32_t t0 = sortedWaypoints.front().first; + + // Build TimedPoint array with normalized time indices + std::vector timedPoints; + timedPoints.reserve(sortedWaypoints.size() + 1); // +1 for wrap point + + // Log first few waypoints for transport 2074 to see conversion + for (size_t idx = 0; idx < sortedWaypoints.size(); idx++) { + const auto& [tMs, pos] = sortedWaypoints[idx]; + + // TransportAnimation.dbc uses server coordinates - convert to canonical + glm::vec3 canonical = core::coords::serverToCanonical(pos); + + // Debug waypoint conversion for first transport (entry 2074) + if (transportEntry == 2074 && idx < 5) { + LOG_INFO("COORD CONVERT: entry=", transportEntry, " t=", tMs, + " serverPos=(", pos.x, ", ", pos.y, ", ", pos.z, ")", + " → canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ")"); + } + + timedPoints.push_back({tMs - t0, canonical}); // Normalize: subtract first timeIndex + } + + // Get base duration from last normalized timeIndex + uint32_t lastTimeMs = sortedWaypoints.back().first - t0; + + // Calculate wrap duration (last → first segment) + // Use average segment duration as wrap duration + uint32_t totalDelta = 0; + int segmentCount = 0; + for (size_t i = 1; i < sortedWaypoints.size(); i++) { + uint32_t delta = sortedWaypoints[i].first - sortedWaypoints[i-1].first; + if (delta > 0) { + totalDelta += delta; + segmentCount++; + } + } + uint32_t wrapMs = (segmentCount > 0) ? (totalDelta / segmentCount) : 1000; + + // Add duplicate first point at end with wrap duration + // This makes the wrap segment (last → first) have proper duration + glm::vec3 firstCanonical = core::coords::serverToCanonical(sortedWaypoints.front().second); + timedPoints.push_back({lastTimeMs + wrapMs, firstCanonical}); + + uint32_t durationMs = lastTimeMs + wrapMs; + + // Store path + TransportPath path; + path.pathId = transportEntry; + path.points = timedPoints; + // Keep looping=true even with duplicate wrap point for smooth control point selection at seam + // This prevents kinks on the last segment approaching the wrap + path.looping = true; + path.durationMs = durationMs; + paths_[transportEntry] = path; + pathsLoaded++; + + // Log first, middle, and last points to verify path data + glm::vec3 firstOffset = timedPoints[0].pos; + size_t midIdx = timedPoints.size() / 2; + glm::vec3 midOffset = timedPoints[midIdx].pos; + glm::vec3 lastOffset = timedPoints[timedPoints.size() - 2].pos; // -2 to skip wrap duplicate + LOG_INFO(" Transport ", transportEntry, ": ", timedPoints.size() - 1, " waypoints + wrap, ", + durationMs, "ms duration (wrap=", wrapMs, "ms, t0_normalized=", timedPoints[0].tMs, "ms)", + " firstOffset=(", firstOffset.x, ", ", firstOffset.y, ", ", firstOffset.z, ")", + " midOffset=(", midOffset.x, ", ", midOffset.y, ", ", midOffset.z, ")", + " lastOffset=(", lastOffset.x, ", ", lastOffset.y, ", ", lastOffset.z, ")"); + } + + LOG_INFO("Loaded ", pathsLoaded, " transport paths from TransportAnimation.dbc"); + return pathsLoaded > 0; +} + +bool TransportManager::hasPathForEntry(uint32_t entry) const { + return paths_.find(entry) != paths_.end(); +} + } // namespace wowee::game diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 84355f1b..8a2161f2 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -634,6 +634,15 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec); + // Log transport-related flag combinations + if (updateFlags & 0x0002) { // UPDATEFLAG_TRANSPORT + LOG_INFO(" Transport flags detected: 0x", std::hex, updateFlags, std::dec, + " (TRANSPORT=", !!(updateFlags & 0x0002), + ", POSITION=", !!(updateFlags & 0x0100), + ", ROTATION=", !!(updateFlags & 0x0200), + ", STATIONARY=", !!(updateFlags & 0x0040), ")"); + } + // UpdateFlags bit meanings: // 0x0001 = UPDATEFLAG_SELF // 0x0002 = UPDATEFLAG_TRANSPORT @@ -771,8 +780,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock } } else if (updateFlags & UPDATEFLAG_POSITION) { - // Transport position update - /*uint64_t transportGuid =*/ readPackedGuid(packet); + // Transport position update (UPDATEFLAG_POSITION = 0x0100) + uint64_t transportGuid = readPackedGuid(packet); block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -783,7 +792,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock /*float corpseOrientation =*/ packet.readFloat(); block.hasMovement = true; - LOG_DEBUG(" POSITION: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation); + LOG_INFO(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec, + " pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation); } else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) { // Simple stationary position (4 floats) diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 4c505dd3..2184b9e6 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2505,6 +2505,28 @@ void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& posit rebuildSpatialIndex(); } +void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) { + auto idxIt = instanceIndexById.find(instanceId); + if (idxIt == instanceIndexById.end()) return; + auto& inst = instances[idxIt->second]; + + // Update model matrix directly + inst.modelMatrix = transform; + inst.invModelMatrix = glm::inverse(transform); + + // Extract position from transform for bounds + inst.position = glm::vec3(transform[3]); + + // Update bounds + auto modelIt = models.find(inst.modelId); + if (modelIt != models.end()) { + glm::vec3 localMin, localMax; + getTightCollisionBounds(modelIt->second, localMin, localMax); + transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax); + } + rebuildSpatialIndex(); +} + void M2Renderer::removeInstance(uint32_t instanceId) { for (auto it = instances.begin(); it != instances.end(); ++it) { if (it->id == instanceId) { diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 0b65102c..8c789e14 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -610,13 +610,29 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0; renderTile = lsbOrder || msbOrder; - // If this tile is masked out, check neighbors to fill gaps - if (!renderTile && x > 0 && y > 0 && x < gridWidth-2 && y < gridHeight-2) { + // If this tile is masked out, check neighbors to fill coastline gaps + if (!renderTile) { // Check adjacent tiles - render if any neighbor is water (blend coastline) for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dx == 0 && dy == 0) continue; - int neighborIdx = (y + dy) * surface.width + (x + dx); + int nx = x + dx; + int ny = y + dy; + // Bounds check neighbors + if (nx < 0 || ny < 0 || nx >= gridWidth-1 || ny >= gridHeight-1) continue; + + // Calculate neighbor mask index (consistent with main tile indexing) + int neighborIdx; + if (surface.wmoId == 0 && surface.mask.size() >= 8) { + // Terrain MH2O: account for xOffset/yOffset + int ncx = static_cast(surface.xOffset) + nx; + int ncy = static_cast(surface.yOffset) + ny; + neighborIdx = ncy * 8 + ncx; + } else { + // WMO/custom: local indexing + neighborIdx = ny * surface.width + nx; + } + int nByteIdx = neighborIdx / 8; int nBitIdx = neighborIdx % 8; if (nByteIdx < static_cast(surface.mask.size())) {