diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5b71d08c..3c186ca9 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -622,6 +622,7 @@ public: void closeTaxi(); void activateTaxi(uint32_t destNodeId); bool isOnTaxiFlight() const { return onTaxiFlight_; } + bool isTaxiMountActive() const { return taxiMountActive_; } const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; } uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; } diff --git a/include/pipeline/asset_manager.hpp b/include/pipeline/asset_manager.hpp index 3a0352ca..6222a017 100644 --- a/include/pipeline/asset_manager.hpp +++ b/include/pipeline/asset_manager.hpp @@ -102,6 +102,7 @@ private: MPQManager mpqManager; mutable std::mutex readMutex; + mutable std::mutex cacheMutex; std::map> dbcCache; // Decompressed file cache (LRU, dynamic budget based on system RAM) diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 0be7f4fe..4f0d3265 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace wowee { @@ -187,6 +188,7 @@ public: void setUnloadRadius(int radius) { unloadRadius = radius; } void setStreamingEnabled(bool enabled) { streamingEnabled = enabled; } void setUpdateInterval(float seconds) { updateInterval = seconds; } + void setTaxiStreamingMode(bool enabled) { taxiStreamingMode_ = enabled; } void setWaterRenderer(WaterRenderer* renderer) { waterRenderer = renderer; } void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; } void setWMORenderer(WMORenderer* renderer) { wmoRenderer = renderer; } @@ -286,6 +288,7 @@ private: int unloadRadius = 7; // Unload tiles beyond this radius float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps) float timeSinceLastUpdate = 0.0f; + bool taxiStreamingMode_ = false; // Tile size constants (WoW ADT specifications) // A tile (ADT) = 16x16 chunks = 533.33 units across @@ -298,7 +301,7 @@ private: int workerCount = 0; std::mutex queueMutex; std::condition_variable queueCV; - std::queue loadQueue; + std::deque loadQueue; std::queue> readyQueue; // In-RAM tile cache (LRU) to avoid re-reading from disk diff --git a/src/core/application.cpp b/src/core/application.cpp index 499f1c5e..5d4331d4 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -447,10 +447,14 @@ void Application::update(float deltaTime) { renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed()); } - bool onTaxi = gameHandler && gameHandler->isOnTaxiFlight(); + bool onTaxi = gameHandler && (gameHandler->isOnTaxiFlight() || gameHandler->isTaxiMountActive()); if (renderer && renderer->getCameraController()) { renderer->getCameraController()->setExternalFollow(onTaxi); renderer->getCameraController()->setExternalMoving(onTaxi); + if (onTaxi) { + // Drop any stale local movement toggles while server drives taxi motion. + renderer->getCameraController()->clearMovementInputs(); + } if (lastTaxiFlight_ && !onTaxi) { renderer->getCameraController()->clearMovementInputs(); } @@ -467,7 +471,12 @@ void Application::update(float deltaTime) { } if (renderer && renderer->getTerrainManager()) { renderer->getTerrainManager()->setStreamingEnabled(true); - renderer->getTerrainManager()->setUpdateInterval(0.1f); + // Slightly slower stream tick on taxi reduces bursty I/O and frame hitches. + renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.2f : 0.1f); + // Keep taxi streaming focused ahead on the route to reduce burst loads. + renderer->getTerrainManager()->setLoadRadius(onTaxi ? 2 : 4); + renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 5 : 7); + renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi); } lastTaxiFlight_ = onTaxi; @@ -489,9 +498,6 @@ void Application::update(float deltaTime) { glm::vec3 canonical(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()); glm::vec3 renderPos = core::coords::canonicalToRender(canonical); renderer->getCharacterPosition() = renderPos; - // Lock yaw to server/taxi orientation so mouse cannot steer during taxi. - float yawDeg = glm::degrees(playerEntity->getOrientation()) + 90.0f; - renderer->setCharacterYaw(yawDeg); } } else if (onTransport) { // Transport mode: compose world position from transport transform + local offset @@ -518,7 +524,7 @@ void Application::update(float deltaTime) { } // Send movement heartbeat every 500ms (keeps server position in sync) - // Skip during taxi flights - server controls position + // Skip periodic taxi heartbeats; taxi start sends explicit heartbeats already. if (gameHandler && renderer && !onTaxi) { movementHeartbeatTimer += deltaTime; if (movementHeartbeatTimer >= 0.5f) { @@ -791,21 +797,27 @@ void Application::setupUICallbacks() { std::set> uniqueTiles; - // Sample waypoints along path and gather tiles - for (const auto& waypoint : path) { + // Sample waypoints along path and gather tiles. + // Use stride to avoid enqueueing huge numbers of tiles at once. + const size_t stride = 4; + for (size_t i = 0; i < path.size(); i += stride) { + const auto& waypoint = path[i]; glm::vec3 renderPos = core::coords::canonicalToRender(waypoint); int tileX = static_cast(32 - (renderPos.x / 533.33333f)); int tileY = static_cast(32 - (renderPos.y / 533.33333f)); - // Load tile at waypoint + 1 radius around it (3x3 per waypoint) - for (int dy = -1; dy <= 1; dy++) { - for (int dx = -1; dx <= 1; dx++) { - int tx = tileX + dx; - int ty = tileY + dy; - if (tx >= 0 && tx <= 63 && ty >= 0 && ty <= 63) { - uniqueTiles.insert({tx, ty}); - } - } + // Precache only the sampled tile itself; terrain streaming handles neighbors. + if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) { + uniqueTiles.insert({tileX, tileY}); + } + } + // Ensure final destination tile is included. + if (!path.empty()) { + glm::vec3 renderPos = core::coords::canonicalToRender(path.back()); + int tileX = static_cast(32 - (renderPos.x / 533.33333f)); + int tileY = static_cast(32 - (renderPos.y / 533.33333f)); + if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) { + uniqueTiles.insert({tileX, tileY}); } } @@ -817,25 +829,22 @@ void Application::setupUICallbacks() { // Taxi orientation callback - update mount rotation during flight gameHandler->setTaxiOrientationCallback([this](float yaw, float pitch, float roll) { if (renderer && renderer->getCameraController()) { - // Convert radians to degrees for camera controller (character facing) + // Taxi callback now provides render-space yaw directly. float yawDegrees = glm::degrees(yaw); renderer->getCameraController()->setFacingYaw(yawDegrees); + renderer->setCharacterYaw(yawDegrees); // Set mount pitch and roll for realistic flight animation renderer->setMountPitchRoll(pitch, roll); } }); - // Taxi flight start callback - upload all precached tiles to GPU before flight begins + // Taxi flight start callback - keep non-blocking to avoid hitching at takeoff. gameHandler->setTaxiFlightStartCallback([this]() { if (renderer && renderer->getTerrainManager() && renderer->getM2Renderer()) { - LOG_INFO("Uploading all precached tiles (terrain + M2 models) to GPU before taxi flight..."); - auto start = std::chrono::steady_clock::now(); - renderer->getTerrainManager()->processAllReadyTiles(); - auto end = std::chrono::steady_clock::now(); - auto durationMs = std::chrono::duration_cast(end - start).count(); + LOG_INFO("Taxi flight start: incremental terrain/M2 streaming active"); uint32_t m2Count = renderer->getM2Renderer()->getModelCount(); uint32_t instCount = renderer->getM2Renderer()->getInstanceCount(); - LOG_INFO("GPU upload completed in ", durationMs, "ms - ", m2Count, " M2 models in VRAM (", instCount, " instances)"); + LOG_INFO("Current M2 VRAM state: ", m2Count, " models (", instCount, " instances)"); } }); @@ -3080,7 +3089,7 @@ void Application::processCreatureSpawnQueue() { retries = it->second; } if (retries < MAX_CREATURE_SPAWN_RETRIES) { - creatureSpawnRetryCounts_[s.guid] = static_cast(retries + 1); + creatureSpawnRetryCounts_[s.guid] = static_cast(retries + 1); pendingCreatureSpawns_.push_back(s); pendingCreatureSpawnGuids_.insert(s.guid); } else { @@ -3176,35 +3185,87 @@ void Application::processPendingMount() { // Apply creature skin textures from CreatureDisplayInfo.dbc. // Re-apply even for cached models so transient failures can self-heal. + std::string modelDir; + size_t lastSlash = m2Path.find_last_of("\\/"); + if (lastSlash != std::string::npos) { + modelDir = m2Path.substr(0, lastSlash + 1); + } + auto itDisplayData = displayDataMap_.find(mountDisplayId); + bool haveDisplayData = false; + CreatureDisplayData dispData{}; if (itDisplayData != displayDataMap_.end()) { - CreatureDisplayData dispData = itDisplayData->second; + dispData = itDisplayData->second; + haveDisplayData = true; + } else { + // Some taxi mount display IDs are sparse; recover skins by matching model path. + std::string lowerMountPath = m2Path; + std::transform(lowerMountPath.begin(), lowerMountPath.end(), lowerMountPath.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + int bestScore = -1; + for (const auto& [dispId, data] : displayDataMap_) { + auto pit = modelIdToPath_.find(data.modelId); + if (pit == modelIdToPath_.end()) continue; + std::string p = pit->second; + std::transform(p.begin(), p.end(), p.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (p != lowerMountPath) continue; + int score = 0; + if (!data.skin1.empty()) { + std::string p1 = modelDir + data.skin1 + ".blp"; + score += assetManager->fileExists(p1) ? 30 : 3; + } + if (!data.skin2.empty()) { + std::string p2 = modelDir + data.skin2 + ".blp"; + score += assetManager->fileExists(p2) ? 20 : 2; + } + if (!data.skin3.empty()) { + std::string p3 = modelDir + data.skin3 + ".blp"; + score += assetManager->fileExists(p3) ? 10 : 1; + } + if (score > bestScore) { + bestScore = score; + dispData = data; + haveDisplayData = true; + } + } + if (haveDisplayData) { + LOG_INFO("Recovered mount display data by model path for displayId=", mountDisplayId, + " skin1='", dispData.skin1, "' skin2='", dispData.skin2, + "' skin3='", dispData.skin3, "'"); + } + } + if (haveDisplayData) { // If this displayId has no skins, try to find another displayId for the same model with skins. if (dispData.skin1.empty() && dispData.skin2.empty() && dispData.skin3.empty()) { - uint32_t modelId = dispData.modelId; + uint32_t sourceModelId = dispData.modelId; int bestScore = -1; for (const auto& [dispId, data] : displayDataMap_) { - if (data.modelId != modelId) continue; + if (data.modelId != sourceModelId) continue; int score = 0; - if (!data.skin1.empty()) score += 3; - if (!data.skin2.empty()) score += 2; - if (!data.skin3.empty()) score += 1; + if (!data.skin1.empty()) { + std::string p = modelDir + data.skin1 + ".blp"; + score += assetManager->fileExists(p) ? 30 : 3; + } + if (!data.skin2.empty()) { + std::string p = modelDir + data.skin2 + ".blp"; + score += assetManager->fileExists(p) ? 20 : 2; + } + if (!data.skin3.empty()) { + std::string p = modelDir + data.skin3 + ".blp"; + score += assetManager->fileExists(p) ? 10 : 1; + } if (score > bestScore) { bestScore = score; dispData = data; } } LOG_INFO("Mount skin fallback for displayId=", mountDisplayId, - " modelId=", modelId, " skin1='", dispData.skin1, + " modelId=", sourceModelId, " skin1='", dispData.skin1, "' skin2='", dispData.skin2, "' skin3='", dispData.skin3, "'"); } const auto* md = charRenderer->getModelData(modelId); if (md) { - std::string modelDir; - size_t lastSlash = m2Path.find_last_of("\\/"); - if (lastSlash != std::string::npos) { - modelDir = m2Path.substr(0, lastSlash + 1); - } int replaced = 0; for (size_t ti = 0; ti < md->textures.size(); ti++) { const auto& tex = md->textures[ti]; @@ -3231,6 +3292,7 @@ void Application::processPendingMount() { if (skinTex != 0) { charRenderer->setModelTexture(modelId, 0, skinTex); LOG_INFO("Forced mount skin1 texture on slot 0: ", texPath); + replaced++; } } else if (replaced == 0 && !md->textures.empty() && !md->textures[0].filename.empty()) { // Last-resort: use the model's first texture filename if it exists. @@ -3238,6 +3300,27 @@ void Application::processPendingMount() { if (texId != 0) { charRenderer->setModelTexture(modelId, 0, texId); LOG_INFO("Forced mount model texture on slot 0: ", md->textures[0].filename); + replaced++; + } + } + + // Final taxi mount fallback for gryphon/wyvern models when display tables are sparse. + if (replaced == 0 && !md->textures.empty()) { + std::string lowerMountPath = m2Path; + std::transform(lowerMountPath.begin(), lowerMountPath.end(), lowerMountPath.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lowerMountPath.find("creature\\gryphon\\gryphon.m2") != std::string::npos) { + GLuint texId = charRenderer->loadTexture("Creature\\Gryphon\\Gryphon.blp"); + if (texId != 0) { + charRenderer->setModelTexture(modelId, 0, texId); + LOG_INFO("Forced canonical gryphon texture on slot 0"); + } + } else if (lowerMountPath.find("creature\\wyvern\\wyvern.m2") != std::string::npos) { + GLuint texId = charRenderer->loadTexture("Creature\\Wyvern\\Wyvern.blp"); + if (texId != 0) { + charRenderer->setModelTexture(modelId, 0, texId); + LOG_INFO("Forced canonical wyvern texture on slot 0"); + } } } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a9f78019..35540fb8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -210,49 +210,58 @@ void GameHandler::update(float deltaTime) { // Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared if (onTaxiFlight_) { updateClientTaxi(deltaTime); - if (!taxiMountActive_ && !taxiClientActive_ && taxiClientPath_.empty()) { - onTaxiFlight_ = false; - LOG_INFO("Cleared stale taxi state in update"); - } auto playerEntity = entityManager.getEntity(playerGuid); - if (playerEntity && playerEntity->getType() == ObjectType::UNIT) { - auto unit = std::static_pointer_cast(playerEntity); - if ((unit->getUnitFlags() & 0x00000100) == 0) { - onTaxiFlight_ = false; - taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering - if (taxiMountActive_ && mountCallback_) { - mountCallback_(0); - } - taxiMountActive_ = false; - taxiMountDisplayId_ = 0; - currentMountDisplayId_ = 0; - taxiClientActive_ = false; - taxiClientPath_.clear(); - taxiRecoverPending_ = false; - movementInfo.flags = 0; - movementInfo.flags2 = 0; - if (socket) { - sendMovement(Opcode::CMSG_MOVE_STOP); - sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); - } - LOG_INFO("Taxi flight landed"); + auto unit = std::dynamic_pointer_cast(playerEntity); + if (unit && + (unit->getUnitFlags() & 0x00000100) == 0 && + !taxiClientActive_ && + !taxiActivatePending_) { + onTaxiFlight_ = false; + taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering + if (taxiMountActive_ && mountCallback_) { + mountCallback_(0); } + taxiMountActive_ = false; + taxiMountDisplayId_ = 0; + currentMountDisplayId_ = 0; + taxiClientActive_ = false; + taxiClientPath_.clear(); + taxiRecoverPending_ = false; + movementInfo.flags = 0; + movementInfo.flags2 = 0; + if (socket) { + sendMovement(Opcode::CMSG_MOVE_STOP); + sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); + } + LOG_INFO("Taxi flight landed"); } } // Safety: if taxi flight ended but mount is still active, force dismount. + // Guard against transient taxi-state flicker. if (!onTaxiFlight_ && taxiMountActive_) { - if (mountCallback_) mountCallback_(0); - taxiMountActive_ = false; - taxiMountDisplayId_ = 0; - currentMountDisplayId_ = 0; - movementInfo.flags = 0; - movementInfo.flags2 = 0; - if (socket) { - sendMovement(Opcode::CMSG_MOVE_STOP); - sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); + bool serverStillTaxi = false; + auto playerEntity = entityManager.getEntity(playerGuid); + auto playerUnit = std::dynamic_pointer_cast(playerEntity); + if (playerUnit) { + serverStillTaxi = (playerUnit->getUnitFlags() & 0x00000100) != 0; + } + + if (serverStillTaxi || taxiClientActive_ || taxiActivatePending_) { + onTaxiFlight_ = true; + } else { + if (mountCallback_) mountCallback_(0); + taxiMountActive_ = false; + taxiMountDisplayId_ = 0; + currentMountDisplayId_ = 0; + movementInfo.flags = 0; + movementInfo.flags2 = 0; + if (socket) { + sendMovement(Opcode::CMSG_MOVE_STOP); + sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); + } + LOG_INFO("Taxi dismount cleanup"); } - LOG_INFO("Taxi dismount cleanup"); } if (taxiRecoverPending_ && state == WorldState::IN_WORLD) { @@ -1696,16 +1705,8 @@ void GameHandler::sendMovement(Opcode opcode) { return; } - // Block movement during taxi flight - if (onTaxiFlight_) { - // If taxi visuals are already gone, clear taxi state to avoid stuck movement. - if (!taxiMountActive_ && !taxiClientActive_ && taxiClientPath_.empty()) { - onTaxiFlight_ = false; - LOG_INFO("Cleared stale taxi state in sendMovement"); - } else { - return; - } - } + // Block manual movement while taxi is active/mounted, but still allow heartbeat packets. + if ((onTaxiFlight_ || taxiMountActive_) && opcode != Opcode::CMSG_MOVE_HEARTBEAT) return; if (resurrectPending_) return; // Use real millisecond timestamp (server validates for anti-cheat) @@ -2346,6 +2347,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.guid == playerGuid) { if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { serverRunSpeed_ = block.runSpeed; + // Some server dismount paths update run speed without updating mount display field. + if (!onTaxiFlight_ && !taxiMountActive_ && + currentMountDisplayId_ != 0 && block.runSpeed <= 8.5f) { + LOG_INFO("Auto-clearing mount from movement speed update: speed=", block.runSpeed, + " displayId=", currentMountDisplayId_); + currentMountDisplayId_ = 0; + if (mountCallback_) { + mountCallback_(0); + } + } } for (const auto& [key, val] : block.fields) { lastPlayerFields_[key] = val; @@ -4158,6 +4169,17 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) { } serverRunSpeed_ = newSpeed; + + // Server can auto-dismount (e.g. entering no-mount areas) and only send a speed change. + // Keep client mount visuals in sync with server-authoritative movement speed. + if (!onTaxiFlight_ && !taxiMountActive_ && currentMountDisplayId_ != 0 && newSpeed <= 8.5f) { + LOG_INFO("Auto-clearing mount from speed change: speed=", newSpeed, + " displayId=", currentMountDisplayId_); + currentMountDisplayId_ = 0; + if (mountCallback_) { + mountCallback_(0); + } + } } // ============================================================ @@ -6057,9 +6079,11 @@ void GameHandler::applyTaxiMountForCurrentNode() { } uint32_t mountId = isAlliance ? it->second.mountDisplayIdAlliance : it->second.mountDisplayIdHorde; + if (mountId == 541) mountId = 0; // Placeholder/invalid in some DBC sets if (mountId == 0) { mountId = isAlliance ? it->second.mountDisplayIdHorde : it->second.mountDisplayIdAlliance; + if (mountId == 541) mountId = 0; } if (mountId == 0) { auto& app = core::Application::getInstance(); @@ -6076,7 +6100,17 @@ void GameHandler::applyTaxiMountForCurrentNode() { if (it->second.mountDisplayIdAlliance != 0) mountId = it->second.mountDisplayIdAlliance; else if (it->second.mountDisplayIdHorde != 0) mountId = it->second.mountDisplayIdHorde; } - if (mountId == 0 || mountId == 541) { + if (mountId == 0) { + // 3.3.5a fallback display IDs (real CreatureDisplayInfo entries). + // Alliance taxi gryphons commonly use 1210-1213. + // Horde taxi wyverns commonly use 1310-1312. + static const uint32_t kAllianceTaxiDisplays[] = {1210u, 1211u, 1212u, 1213u}; + static const uint32_t kHordeTaxiDisplays[] = {1310u, 1311u, 1312u}; + mountId = isAlliance ? kAllianceTaxiDisplays[0] : kHordeTaxiDisplays[0]; + } + + // Last resort legacy fallback. + if (mountId == 0) { mountId = isAlliance ? 30412u : 30413u; } if (mountId != 0) { @@ -6122,34 +6156,58 @@ void GameHandler::startClientTaxiPath(const std::vector& pathNodes) { } } + if (taxiClientPath_.size() < 2) { + // Fallback: use TaxiNodes directly when TaxiPathNode spline data is missing. + 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; } - // Set initial orientation to face the first flight segment - if (!entityManager.hasEntity(playerGuid)) return; + // Set initial orientation to face the first non-degenerate flight segment. + glm::vec3 start = taxiClientPath_[0]; + glm::vec3 dir(0.0f); + float dirLen = 0.0f; + for (size_t i = 1; i < taxiClientPath_.size(); i++) { + dir = taxiClientPath_[i] - start; + dirLen = glm::length(dir); + if (dirLen >= 0.001f) { + break; + } + } + + float initialOrientation = movementInfo.orientation; + float initialRenderYaw = movementInfo.orientation; + float initialPitch = 0.0f; + float initialRoll = 0.0f; + if (dirLen >= 0.001f) { + 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 / dirLen; + 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; + auto playerEntity = entityManager.getEntity(playerGuid); if (playerEntity) { - glm::vec3 start = taxiClientPath_[0]; - glm::vec3 end = taxiClientPath_[1]; - glm::vec3 dir = end - start; - float dirLen = glm::length(dir); - if (dirLen < 0.001f) return; - float initialOrientation = std::atan2(dir.y, dir.x) - 1.57079632679f; - - // Calculate initial pitch from altitude change - glm::vec3 dirNorm = dir / dirLen; - float initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f)); - float initialRoll = 0.0f; // No initial banking - playerEntity->setPosition(start.x, start.y, start.z, initialOrientation); - movementInfo.orientation = initialOrientation; + } - // Update mount rotation immediately with pitch and roll - if (taxiOrientationCallback_) { - taxiOrientationCallback_(initialOrientation, initialPitch, initialRoll); - } + if (taxiOrientationCallback_) { + taxiOrientationCallback_(initialRenderYaw, initialPitch, initialRoll); } LOG_INFO("Taxi flight started with ", taxiClientPath_.size(), " spline waypoints"); @@ -6158,31 +6216,9 @@ void GameHandler::startClientTaxiPath(const std::vector& pathNodes) { void GameHandler::updateClientTaxi(float deltaTime) { if (!taxiClientActive_ || taxiClientPath_.size() < 2) return; - if (!entityManager.hasEntity(playerGuid)) return; auto playerEntity = entityManager.getEntity(playerGuid); - if (!playerEntity) return; - if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { - taxiClientActive_ = false; - return; - } - - glm::vec3 start = taxiClientPath_[taxiClientIndex_]; - glm::vec3 end = taxiClientPath_[taxiClientIndex_ + 1]; - glm::vec3 dir = end - start; - float segmentLen = glm::length(dir); - if (segmentLen < 0.01f) { - taxiClientIndex_++; - taxiClientSegmentProgress_ = 0.0f; - return; - } - - taxiClientSegmentProgress_ += taxiClientSpeed_ * deltaTime; - float t = taxiClientSegmentProgress_ / segmentLen; - if (t >= 1.0f) { - taxiClientIndex_++; - taxiClientSegmentProgress_ = 0.0f; - if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { + auto finishTaxiFlight = [&]() { taxiClientActive_ = false; onTaxiFlight_ = false; taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering @@ -6201,10 +6237,50 @@ void GameHandler::updateClientTaxi(float deltaTime) { sendMovement(Opcode::CMSG_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; + + // Consume as many tiny/finished segments as needed this frame so taxi doesn't stall + // on dense/degenerate node clusters near takeoff/landing. + while (true) { + if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { + finishTaxiFlight(); + return; + } + + start = taxiClientPath_[taxiClientIndex_]; + end = taxiClientPath_[taxiClientIndex_ + 1]; + dir = end - start; + segmentLen = glm::length(dir); + + if (segmentLen < 0.01f) { + taxiClientIndex_++; + continue; + } + + if (remainingDistance >= segmentLen) { + remainingDistance -= segmentLen; + taxiClientIndex_++; + taxiClientSegmentProgress_ = 0.0f; + continue; + } + + taxiClientSegmentProgress_ = remainingDistance; + t = taxiClientSegmentProgress_ / segmentLen; + break; + } + // Use Catmull-Rom spline for smooth interpolation between waypoints // Get surrounding points for spline curve glm::vec3 p0 = (taxiClientIndex_ > 0) ? taxiClientPath_[taxiClientIndex_ - 1] : start; @@ -6240,7 +6316,7 @@ void GameHandler::updateClientTaxi(float deltaTime) { } // Calculate yaw from horizontal direction - float targetOrientation = std::atan2(tangent.y, tangent.x) - 1.57079632679f; + float targetOrientation = std::atan2(tangent.y, tangent.x); // Calculate pitch from vertical component (altitude change) glm::vec3 tangentNorm = tangent / std::max(tangentLen, 0.0001f); @@ -6259,15 +6335,20 @@ void GameHandler::updateClientTaxi(float deltaTime) { // Smooth rotation transition (lerp towards target) float smoothOrientation = currentOrientation + orientDiff * std::min(1.0f, deltaTime * 3.0f); - playerEntity->setPosition(nextPos.x, nextPos.y, nextPos.z, smoothOrientation); + 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; - // Update mount rotation with yaw, pitch, and roll for realistic flight + // Update mount rotation with yaw/pitch/roll. Use render-space tangent yaw to + // avoid canonical<->render convention mismatches. if (taxiOrientationCallback_) { - taxiOrientationCallback_(smoothOrientation, pitch, roll); + glm::vec3 renderTangent = core::coords::canonicalToRender(tangent); + float renderYaw = std::atan2(renderTangent.y, renderTangent.x); + taxiOrientationCallback_(renderYaw, pitch, roll); } } @@ -6279,13 +6360,19 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) { } if (data.result == 0) { + // Some cores can emit duplicate success replies (e.g. basic + express activate). + // Ignore repeats once taxi is already active and no activation is pending. + if (onTaxiFlight_ && !taxiActivatePending_) { + return; + } onTaxiFlight_ = true; taxiWindowOpen_ = false; - taxiMountActive_ = false; - taxiMountDisplayId_ = 0; taxiActivatePending_ = false; taxiActivateTimer_ = 0.0f; applyTaxiMountForCurrentNode(); + if (socket) { + sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); + } LOG_INFO("Taxi flight started!"); } else { LOG_WARNING("Taxi activation failed, result=", data.result); @@ -6438,6 +6525,9 @@ void GameHandler::activateTaxi(uint32_t destNodeId) { onTaxiFlight_ = true; applyTaxiMountForCurrentNode(); } + if (socket) { + sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); + } // Trigger terrain precache immediately (non-blocking). if (taxiPrecacheCallback_) { diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 5a4f81d2..defb0517 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -26,10 +26,11 @@ bool AssetManager::initialize(const std::string& dataPath_) { return false; } - // Set dynamic file cache budget based on available RAM + // Set dynamic file cache budget based on available RAM. + // Bias toward MPQ decompressed-file caching to minimize runtime read/decompress stalls. auto& memMonitor = core::MemoryMonitor::getInstance(); size_t recommendedBudget = memMonitor.getRecommendedCacheBudget(); - fileCacheBudget = recommendedBudget / 2; // Split budget: half for file cache, half for other caches + fileCacheBudget = (recommendedBudget * 3) / 4; // 75% of global cache budget initialized = true; LOG_INFO("Asset manager initialized (dynamic file cache: ", @@ -152,20 +153,26 @@ std::vector AssetManager::readFile(const std::string& path) const { } std::string normalized = normalizePath(path); - std::lock_guard lock(readMutex); - - // Check cache first - auto it = fileCache.find(normalized); - if (it != fileCache.end()) { - // Cache hit - update access time and return cached data - it->second.lastAccessTime = ++fileCacheAccessCounter; - fileCacheHits++; - return it->second.data; + { + std::lock_guard cacheLock(cacheMutex); + // Check cache first + auto it = fileCache.find(normalized); + if (it != fileCache.end()) { + // Cache hit - update access time and return cached data + it->second.lastAccessTime = ++fileCacheAccessCounter; + fileCacheHits++; + return it->second.data; + } + fileCacheMisses++; } - // Cache miss - decompress from MPQ - fileCacheMisses++; - std::vector data = mpqManager.readFile(normalized); + // Cache miss - decompress from MPQ. + // Keep MPQ reads serialized, but do not block cache-hit readers on this mutex. + std::vector data; + { + std::lock_guard readLock(readMutex); + data = mpqManager.readFile(normalized); + } if (data.empty()) { return data; // File not found } @@ -173,6 +180,7 @@ std::vector AssetManager::readFile(const std::string& path) const { // Add to cache if within budget size_t fileSize = data.size(); if (fileSize > 0 && fileSize < fileCacheBudget / 2) { // Don't cache files > 50% of budget (very aggressive) + std::lock_guard cacheLock(cacheMutex); // Evict old entries if needed (LRU) while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) { // Find least recently used entry @@ -198,7 +206,7 @@ std::vector AssetManager::readFile(const std::string& path) const { } void AssetManager::clearCache() { - std::lock_guard lock(readMutex); + std::scoped_lock lock(readMutex, cacheMutex); dbcCache.clear(); fileCache.clear(); fileCacheTotalBytes = 0; @@ -211,6 +219,9 @@ std::string AssetManager::normalizePath(const std::string& path) const { // Convert forward slashes to backslashes (WoW uses backslashes) std::replace(normalized.begin(), normalized.end(), '/', '\\'); + // Lowercase for case-insensitive cache keys (improves hit rate across mixed-case callers). + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); return normalized; } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 7eee46a1..fab9fcc1 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -112,17 +112,12 @@ void CameraController::update(float deltaTime) { return; } - // During taxi flights, skip input/movement logic but still position camera + // During taxi flights, skip movement logic but keep camera orbit/zoom controls. if (externalFollow_) { - // Mouse look (right mouse button) - if (rightMouseDown) { - int mouseDX, mouseDY; - SDL_GetRelativeMouseState(&mouseDX, &mouseDY); - yaw -= mouseDX * mouseSensitivity; - pitch -= mouseDY * mouseSensitivity; - pitch = glm::clamp(pitch, -89.0f, 89.0f); - camera->setRotation(yaw, pitch); - } + camera->setRotation(yaw, pitch); + float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime); + currentDistance += (userTargetDistance - currentDistance) * zoomLerp; + collisionDistance = currentDistance; // Position camera behind character during taxi if (thirdPerson && followTarget) { diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index 88a91796..ad34506f 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -125,12 +125,9 @@ void Celestial::shutdown() { void Celestial::render(const Camera& camera, float timeOfDay, const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) { if (!renderingEnabled || vao == 0 || !celestialShader) { - LOG_WARNING("Celestial render blocked: enabled=", renderingEnabled, " vao=", vao, " shader=", (celestialShader ? "ok" : "null")); return; } - LOG_INFO("Celestial render: timeOfDay=", timeOfDay, " gameTime=", gameTime); - // Update moon phases from game time if available (deterministic) if (gameTime >= 0.0f) { updatePhasesFromGameTime(gameTime); @@ -166,22 +163,16 @@ void Celestial::renderSun(const Camera& camera, float timeOfDay, const glm::vec3* sunDir, const glm::vec3* sunColor) { // Sun visible from 5:00 to 19:00 if (timeOfDay < 5.0f || timeOfDay >= 19.0f) { - LOG_INFO("Sun not visible: timeOfDay=", timeOfDay, " (visible 5:00-19:00)"); return; } - LOG_INFO("Rendering sun: timeOfDay=", timeOfDay, " sunDir=", (sunDir ? "yes" : "no"), " sunColor=", (sunColor ? "yes" : "no")); - celestialShader->use(); - // TESTING: Try X-up (final axis test) - glm::vec3 dir = glm::normalize(glm::vec3(1.0f, 0.0f, 0.0f)); // X-up test - LOG_INFO("Sun direction (TESTING X-UP): dir=(", dir.x, ",", dir.y, ",", dir.z, ")"); + glm::vec3 dir = sunDir ? glm::normalize(*sunDir) : glm::vec3(0.0f, 0.0f, 1.0f); // Place sun on sky sphere at fixed distance const float sunDistance = 800.0f; glm::vec3 sunPos = dir * sunDistance; - LOG_INFO("Sun position: dir * ", sunDistance, " = (", sunPos.x, ",", sunPos.y, ",", sunPos.z, ")"); // Create model matrix glm::mat4 model = glm::mat4(1.0f); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 6bc18f67..acd43e38 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -957,13 +957,9 @@ void Renderer::updateCharacterAnimation() { // Sync mount instance position and rotation float mountBob = 0.0f; + float mountYawRad = glm::radians(characterYaw); if (mountInstanceId_ > 0) { characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); - float yawRad = glm::radians(characterYaw); - if (taxiFlight_) { - // Taxi mounts commonly use a different model-forward axis than player rigs. - yawRad += 1.57079632679f; - } // Procedural lean into turns (ground mounts only, optional enhancement) if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { @@ -982,7 +978,7 @@ void Renderer::updateCharacterAnimation() { } // Apply pitch (up/down), roll (banking), and yaw for realistic flight - characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, yawRad)); + characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, mountYawRad)); // Drive mount model animation: idle when still, run when moving auto pickMountAnim = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { @@ -1172,7 +1168,42 @@ void Renderer::updateCharacterAnimation() { } } - // Use mount's attachment point for proper bone-driven rider positioning + // Use mount's attachment point for proper bone-driven rider positioning. + if (taxiFlight_) { + glm::mat4 mountSeatTransform(1.0f); + bool haveSeat = false; + static constexpr uint32_t kTaxiSeatAttachmentId = 0; // deterministic rider seat + if (mountSeatAttachmentId_ == -1) { + mountSeatAttachmentId_ = static_cast(kTaxiSeatAttachmentId); + } + if (mountSeatAttachmentId_ >= 0) { + haveSeat = characterRenderer->getAttachmentTransform( + mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); + } + if (!haveSeat) { + mountSeatAttachmentId_ = -2; + } + + if (haveSeat) { + glm::vec3 targetRiderPos = glm::vec3(mountSeatTransform[3]) + glm::vec3(0.0f, 0.0f, 0.02f); + // Taxi passengers should be rigidly parented to mount attachment transforms. + // Smoothing here introduces visible seat lag/drift on turns. + mountSeatSmoothingInit_ = false; + smoothedMountSeatPos_ = targetRiderPos; + characterRenderer->setInstancePosition(characterInstanceId, targetRiderPos); + } else { + mountSeatSmoothingInit_ = false; + glm::vec3 playerPos = characterPosition + glm::vec3(0.0f, 0.0f, mountHeightOffset_ + 0.10f); + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + } + + float riderPitch = mountPitch_ * 0.35f; + float riderRoll = mountRoll_ * 0.35f; + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, mountYawRad)); + return; + } + + // Ground mounts: try a seat attachment first. glm::mat4 mountSeatTransform; bool haveSeat = false; if (mountSeatAttachmentId_ >= 0) { diff --git a/src/rendering/sky_system.cpp b/src/rendering/sky_system.cpp index 920e7b61..9e037130 100644 --- a/src/rendering/sky_system.cpp +++ b/src/rendering/sky_system.cpp @@ -146,14 +146,11 @@ void SkySystem::render(const Camera& camera, const SkyParams& params) { } glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const { - // TESTING: X-up test - glm::vec3 dir = glm::vec3(1.0f, 0.0f, 0.0f); // X-up - glm::vec3 pos = dir * 800.0f; - - static int counter = 0; - if (counter++ % 100 == 0) { - LOG_INFO("Flare TEST X-UP dir=(", dir.x, ",", dir.y, ",", dir.z, ") pos=(", pos.x, ",", pos.y, ",", pos.z, ")"); + glm::vec3 dir = glm::normalize(params.directionalDir); + if (glm::length(dir) < 0.0001f) { + dir = glm::vec3(0.0f, 0.0f, 1.0f); } + glm::vec3 pos = dir * 800.0f; return pos; } diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index b49c3d0d..6eec76d6 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -115,9 +115,10 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* return false; } - // Set dynamic tile cache budget (use other half of recommended budget) + // Set dynamic tile cache budget. + // Keep this lower so decompressed MPQ file cache can stay very aggressive. auto& memMonitor = core::MemoryMonitor::getInstance(); - tileCacheBudgetBytes_ = memMonitor.getRecommendedCacheBudget() / 2; + tileCacheBudgetBytes_ = memMonitor.getRecommendedCacheBudget() / 4; LOG_INFO("Terrain tile cache budget: ", tileCacheBudgetBytes_ / (1024 * 1024), " MB (dynamic)"); // Start background worker pool (dynamic: scales with available cores) @@ -222,7 +223,7 @@ bool TerrainManager::enqueueTile(int x, int y) { { std::lock_guard lock(queueMutex); - loadQueue.push(coord); + loadQueue.push_back(coord); pendingTiles[coord] = true; } queueCV.notify_all(); @@ -791,7 +792,7 @@ void TerrainManager::workerLoop() { if (!loadQueue.empty()) { coord = loadQueue.front(); - loadQueue.pop(); + loadQueue.pop_front(); hasWork = true; } } @@ -1056,7 +1057,7 @@ void TerrainManager::unloadAll() { // Clear queues { std::lock_guard lock(queueMutex); - while (!loadQueue.empty()) loadQueue.pop(); + while (!loadQueue.empty()) loadQueue.pop_front(); while (!readyQueue.empty()) readyQueue.pop(); } pendingTiles.clear(); @@ -1353,7 +1354,7 @@ void TerrainManager::streamTiles() { if (pendingTiles.find(coord) != pendingTiles.end()) continue; if (failedTiles.find(coord) != failedTiles.end()) continue; - loadQueue.push(coord); + loadQueue.push_back(coord); pendingTiles[coord] = true; } } @@ -1409,7 +1410,9 @@ void TerrainManager::precacheTiles(const std::vector>& tiles if (pendingTiles.find(coord) != pendingTiles.end()) continue; if (failedTiles.find(coord) != failedTiles.end()) continue; - loadQueue.push(coord); + // Precache work is prioritized so taxi-route tiles are prepared before + // opportunistic radius streaming tiles. + loadQueue.push_front(coord); pendingTiles[coord] = true; }