Fix taxi startup/attachment and reduce taxi streaming hitches

This commit is contained in:
Kelsi 2026-02-11 19:28:15 -08:00
parent f752a4f517
commit 38cef8d9c6
11 changed files with 396 additions and 190 deletions

View file

@ -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; }

View file

@ -102,6 +102,7 @@ private:
MPQManager mpqManager;
mutable std::mutex readMutex;
mutable std::mutex cacheMutex;
std::map<std::string, std::shared_ptr<DBCFile>> dbcCache;
// Decompressed file cache (LRU, dynamic budget based on system RAM)

View file

@ -17,6 +17,7 @@
#include <list>
#include <vector>
#include <condition_variable>
#include <deque>
#include <glm/glm.hpp>
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<TileCoord> loadQueue;
std::deque<TileCoord> loadQueue;
std::queue<std::shared_ptr<PendingTile>> readyQueue;
// In-RAM tile cache (LRU) to avoid re-reading from disk

View file

@ -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<std::pair<int, int>> 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<int>(32 - (renderPos.x / 533.33333f));
int tileY = static_cast<int>(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<int>(32 - (renderPos.x / 533.33333f));
int tileY = static_cast<int>(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<std::chrono::milliseconds>(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<uint8_t>(retries + 1);
creatureSpawnRetryCounts_[s.guid] = static_cast<uint16_t>(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<char>(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<char>(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<char>(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");
}
}
}
}

View file

@ -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<Unit>(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<Unit>(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<Unit>(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<uint32_t>& 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<uint32_t>& 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_) {

View file

@ -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<uint8_t> AssetManager::readFile(const std::string& path) const {
}
std::string normalized = normalizePath(path);
std::lock_guard<std::mutex> 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<std::mutex> 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<uint8_t> 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<uint8_t> data;
{
std::lock_guard<std::mutex> readLock(readMutex);
data = mpqManager.readFile(normalized);
}
if (data.empty()) {
return data; // File not found
}
@ -173,6 +180,7 @@ std::vector<uint8_t> 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<std::mutex> 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<uint8_t> AssetManager::readFile(const std::string& path) const {
}
void AssetManager::clearCache() {
std::lock_guard<std::mutex> 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<char>(std::tolower(c)); });
return normalized;
}

View file

@ -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) {

View file

@ -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);

View file

@ -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<uint32_t> 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<int>(kTaxiSeatAttachmentId);
}
if (mountSeatAttachmentId_ >= 0) {
haveSeat = characterRenderer->getAttachmentTransform(
mountInstanceId_, static_cast<uint32_t>(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) {

View file

@ -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;
}

View file

@ -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<std::mutex> 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<std::mutex> 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<std::pair<int, int>>& 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;
}