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 closeTaxi();
void activateTaxi(uint32_t destNodeId); void activateTaxi(uint32_t destNodeId);
bool isOnTaxiFlight() const { return onTaxiFlight_; } bool isOnTaxiFlight() const { return onTaxiFlight_; }
bool isTaxiMountActive() const { return taxiMountActive_; }
const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; } const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; }
uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; } uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; }

View file

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

View file

@ -17,6 +17,7 @@
#include <list> #include <list>
#include <vector> #include <vector>
#include <condition_variable> #include <condition_variable>
#include <deque>
#include <glm/glm.hpp> #include <glm/glm.hpp>
namespace wowee { namespace wowee {
@ -187,6 +188,7 @@ public:
void setUnloadRadius(int radius) { unloadRadius = radius; } void setUnloadRadius(int radius) { unloadRadius = radius; }
void setStreamingEnabled(bool enabled) { streamingEnabled = enabled; } void setStreamingEnabled(bool enabled) { streamingEnabled = enabled; }
void setUpdateInterval(float seconds) { updateInterval = seconds; } void setUpdateInterval(float seconds) { updateInterval = seconds; }
void setTaxiStreamingMode(bool enabled) { taxiStreamingMode_ = enabled; }
void setWaterRenderer(WaterRenderer* renderer) { waterRenderer = renderer; } void setWaterRenderer(WaterRenderer* renderer) { waterRenderer = renderer; }
void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; } void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; }
void setWMORenderer(WMORenderer* renderer) { wmoRenderer = renderer; } void setWMORenderer(WMORenderer* renderer) { wmoRenderer = renderer; }
@ -286,6 +288,7 @@ private:
int unloadRadius = 7; // Unload tiles beyond this radius int unloadRadius = 7; // Unload tiles beyond this radius
float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps) float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps)
float timeSinceLastUpdate = 0.0f; float timeSinceLastUpdate = 0.0f;
bool taxiStreamingMode_ = false;
// Tile size constants (WoW ADT specifications) // Tile size constants (WoW ADT specifications)
// A tile (ADT) = 16x16 chunks = 533.33 units across // A tile (ADT) = 16x16 chunks = 533.33 units across
@ -298,7 +301,7 @@ private:
int workerCount = 0; int workerCount = 0;
std::mutex queueMutex; std::mutex queueMutex;
std::condition_variable queueCV; std::condition_variable queueCV;
std::queue<TileCoord> loadQueue; std::deque<TileCoord> loadQueue;
std::queue<std::shared_ptr<PendingTile>> readyQueue; std::queue<std::shared_ptr<PendingTile>> readyQueue;
// In-RAM tile cache (LRU) to avoid re-reading from disk // 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()); renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
} }
bool onTaxi = gameHandler && gameHandler->isOnTaxiFlight(); bool onTaxi = gameHandler && (gameHandler->isOnTaxiFlight() || gameHandler->isTaxiMountActive());
if (renderer && renderer->getCameraController()) { if (renderer && renderer->getCameraController()) {
renderer->getCameraController()->setExternalFollow(onTaxi); renderer->getCameraController()->setExternalFollow(onTaxi);
renderer->getCameraController()->setExternalMoving(onTaxi); renderer->getCameraController()->setExternalMoving(onTaxi);
if (onTaxi) {
// Drop any stale local movement toggles while server drives taxi motion.
renderer->getCameraController()->clearMovementInputs();
}
if (lastTaxiFlight_ && !onTaxi) { if (lastTaxiFlight_ && !onTaxi) {
renderer->getCameraController()->clearMovementInputs(); renderer->getCameraController()->clearMovementInputs();
} }
@ -467,7 +471,12 @@ void Application::update(float deltaTime) {
} }
if (renderer && renderer->getTerrainManager()) { if (renderer && renderer->getTerrainManager()) {
renderer->getTerrainManager()->setStreamingEnabled(true); 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; lastTaxiFlight_ = onTaxi;
@ -489,9 +498,6 @@ void Application::update(float deltaTime) {
glm::vec3 canonical(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()); glm::vec3 canonical(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ());
glm::vec3 renderPos = core::coords::canonicalToRender(canonical); glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
renderer->getCharacterPosition() = renderPos; 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) { } else if (onTransport) {
// Transport mode: compose world position from transport transform + local offset // 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) // 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) { if (gameHandler && renderer && !onTaxi) {
movementHeartbeatTimer += deltaTime; movementHeartbeatTimer += deltaTime;
if (movementHeartbeatTimer >= 0.5f) { if (movementHeartbeatTimer >= 0.5f) {
@ -791,21 +797,27 @@ void Application::setupUICallbacks() {
std::set<std::pair<int, int>> uniqueTiles; std::set<std::pair<int, int>> uniqueTiles;
// Sample waypoints along path and gather tiles // Sample waypoints along path and gather tiles.
for (const auto& waypoint : path) { // 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); glm::vec3 renderPos = core::coords::canonicalToRender(waypoint);
int tileX = static_cast<int>(32 - (renderPos.x / 533.33333f)); int tileX = static_cast<int>(32 - (renderPos.x / 533.33333f));
int tileY = static_cast<int>(32 - (renderPos.y / 533.33333f)); int tileY = static_cast<int>(32 - (renderPos.y / 533.33333f));
// Load tile at waypoint + 1 radius around it (3x3 per waypoint) // Precache only the sampled tile itself; terrain streaming handles neighbors.
for (int dy = -1; dy <= 1; dy++) { if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
for (int dx = -1; dx <= 1; dx++) { uniqueTiles.insert({tileX, tileY});
int tx = tileX + dx; }
int ty = tileY + dy; }
if (tx >= 0 && tx <= 63 && ty >= 0 && ty <= 63) { // Ensure final destination tile is included.
uniqueTiles.insert({tx, ty}); 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 // Taxi orientation callback - update mount rotation during flight
gameHandler->setTaxiOrientationCallback([this](float yaw, float pitch, float roll) { gameHandler->setTaxiOrientationCallback([this](float yaw, float pitch, float roll) {
if (renderer && renderer->getCameraController()) { 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); float yawDegrees = glm::degrees(yaw);
renderer->getCameraController()->setFacingYaw(yawDegrees); renderer->getCameraController()->setFacingYaw(yawDegrees);
renderer->setCharacterYaw(yawDegrees);
// Set mount pitch and roll for realistic flight animation // Set mount pitch and roll for realistic flight animation
renderer->setMountPitchRoll(pitch, roll); 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]() { gameHandler->setTaxiFlightStartCallback([this]() {
if (renderer && renderer->getTerrainManager() && renderer->getM2Renderer()) { if (renderer && renderer->getTerrainManager() && renderer->getM2Renderer()) {
LOG_INFO("Uploading all precached tiles (terrain + M2 models) to GPU before taxi flight..."); LOG_INFO("Taxi flight start: incremental terrain/M2 streaming active");
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();
uint32_t m2Count = renderer->getM2Renderer()->getModelCount(); uint32_t m2Count = renderer->getM2Renderer()->getModelCount();
uint32_t instCount = renderer->getM2Renderer()->getInstanceCount(); 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; retries = it->second;
} }
if (retries < MAX_CREATURE_SPAWN_RETRIES) { 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); pendingCreatureSpawns_.push_back(s);
pendingCreatureSpawnGuids_.insert(s.guid); pendingCreatureSpawnGuids_.insert(s.guid);
} else { } else {
@ -3176,35 +3185,87 @@ void Application::processPendingMount() {
// Apply creature skin textures from CreatureDisplayInfo.dbc. // Apply creature skin textures from CreatureDisplayInfo.dbc.
// Re-apply even for cached models so transient failures can self-heal. // 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); auto itDisplayData = displayDataMap_.find(mountDisplayId);
bool haveDisplayData = false;
CreatureDisplayData dispData{};
if (itDisplayData != displayDataMap_.end()) { 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 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()) { if (dispData.skin1.empty() && dispData.skin2.empty() && dispData.skin3.empty()) {
uint32_t modelId = dispData.modelId; uint32_t sourceModelId = dispData.modelId;
int bestScore = -1; int bestScore = -1;
for (const auto& [dispId, data] : displayDataMap_) { for (const auto& [dispId, data] : displayDataMap_) {
if (data.modelId != modelId) continue; if (data.modelId != sourceModelId) continue;
int score = 0; int score = 0;
if (!data.skin1.empty()) score += 3; if (!data.skin1.empty()) {
if (!data.skin2.empty()) score += 2; std::string p = modelDir + data.skin1 + ".blp";
if (!data.skin3.empty()) score += 1; 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) { if (score > bestScore) {
bestScore = score; bestScore = score;
dispData = data; dispData = data;
} }
} }
LOG_INFO("Mount skin fallback for displayId=", mountDisplayId, LOG_INFO("Mount skin fallback for displayId=", mountDisplayId,
" modelId=", modelId, " skin1='", dispData.skin1, " modelId=", sourceModelId, " skin1='", dispData.skin1,
"' skin2='", dispData.skin2, "' skin3='", dispData.skin3, "'"); "' skin2='", dispData.skin2, "' skin3='", dispData.skin3, "'");
} }
const auto* md = charRenderer->getModelData(modelId); const auto* md = charRenderer->getModelData(modelId);
if (md) { 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; int replaced = 0;
for (size_t ti = 0; ti < md->textures.size(); ti++) { for (size_t ti = 0; ti < md->textures.size(); ti++) {
const auto& tex = md->textures[ti]; const auto& tex = md->textures[ti];
@ -3231,6 +3292,7 @@ void Application::processPendingMount() {
if (skinTex != 0) { if (skinTex != 0) {
charRenderer->setModelTexture(modelId, 0, skinTex); charRenderer->setModelTexture(modelId, 0, skinTex);
LOG_INFO("Forced mount skin1 texture on slot 0: ", texPath); LOG_INFO("Forced mount skin1 texture on slot 0: ", texPath);
replaced++;
} }
} else if (replaced == 0 && !md->textures.empty() && !md->textures[0].filename.empty()) { } else if (replaced == 0 && !md->textures.empty() && !md->textures[0].filename.empty()) {
// Last-resort: use the model's first texture filename if it exists. // Last-resort: use the model's first texture filename if it exists.
@ -3238,6 +3300,27 @@ void Application::processPendingMount() {
if (texId != 0) { if (texId != 0) {
charRenderer->setModelTexture(modelId, 0, texId); charRenderer->setModelTexture(modelId, 0, texId);
LOG_INFO("Forced mount model texture on slot 0: ", md->textures[0].filename); 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 // Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
if (onTaxiFlight_) { if (onTaxiFlight_) {
updateClientTaxi(deltaTime); updateClientTaxi(deltaTime);
if (!taxiMountActive_ && !taxiClientActive_ && taxiClientPath_.empty()) {
onTaxiFlight_ = false;
LOG_INFO("Cleared stale taxi state in update");
}
auto playerEntity = entityManager.getEntity(playerGuid); auto playerEntity = entityManager.getEntity(playerGuid);
if (playerEntity && playerEntity->getType() == ObjectType::UNIT) { auto unit = std::dynamic_pointer_cast<Unit>(playerEntity);
auto unit = std::static_pointer_cast<Unit>(playerEntity); if (unit &&
if ((unit->getUnitFlags() & 0x00000100) == 0) { (unit->getUnitFlags() & 0x00000100) == 0 &&
onTaxiFlight_ = false; !taxiClientActive_ &&
taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering !taxiActivatePending_) {
if (taxiMountActive_ && mountCallback_) { onTaxiFlight_ = false;
mountCallback_(0); taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering
} if (taxiMountActive_ && mountCallback_) {
taxiMountActive_ = false; mountCallback_(0);
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");
} }
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. // Safety: if taxi flight ended but mount is still active, force dismount.
// Guard against transient taxi-state flicker.
if (!onTaxiFlight_ && taxiMountActive_) { if (!onTaxiFlight_ && taxiMountActive_) {
if (mountCallback_) mountCallback_(0); bool serverStillTaxi = false;
taxiMountActive_ = false; auto playerEntity = entityManager.getEntity(playerGuid);
taxiMountDisplayId_ = 0; auto playerUnit = std::dynamic_pointer_cast<Unit>(playerEntity);
currentMountDisplayId_ = 0; if (playerUnit) {
movementInfo.flags = 0; serverStillTaxi = (playerUnit->getUnitFlags() & 0x00000100) != 0;
movementInfo.flags2 = 0; }
if (socket) {
sendMovement(Opcode::CMSG_MOVE_STOP); if (serverStillTaxi || taxiClientActive_ || taxiActivatePending_) {
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); 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) { if (taxiRecoverPending_ && state == WorldState::IN_WORLD) {
@ -1696,16 +1705,8 @@ void GameHandler::sendMovement(Opcode opcode) {
return; return;
} }
// Block movement during taxi flight // Block manual movement while taxi is active/mounted, but still allow heartbeat packets.
if (onTaxiFlight_) { if ((onTaxiFlight_ || taxiMountActive_) && opcode != Opcode::CMSG_MOVE_HEARTBEAT) return;
// 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;
}
}
if (resurrectPending_) return; if (resurrectPending_) return;
// Use real millisecond timestamp (server validates for anti-cheat) // 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.guid == playerGuid) {
if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
serverRunSpeed_ = block.runSpeed; 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) { for (const auto& [key, val] : block.fields) {
lastPlayerFields_[key] = val; lastPlayerFields_[key] = val;
@ -4158,6 +4169,17 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
} }
serverRunSpeed_ = newSpeed; 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 uint32_t mountId = isAlliance ? it->second.mountDisplayIdAlliance
: it->second.mountDisplayIdHorde; : it->second.mountDisplayIdHorde;
if (mountId == 541) mountId = 0; // Placeholder/invalid in some DBC sets
if (mountId == 0) { if (mountId == 0) {
mountId = isAlliance ? it->second.mountDisplayIdHorde mountId = isAlliance ? it->second.mountDisplayIdHorde
: it->second.mountDisplayIdAlliance; : it->second.mountDisplayIdAlliance;
if (mountId == 541) mountId = 0;
} }
if (mountId == 0) { if (mountId == 0) {
auto& app = core::Application::getInstance(); auto& app = core::Application::getInstance();
@ -6076,7 +6100,17 @@ void GameHandler::applyTaxiMountForCurrentNode() {
if (it->second.mountDisplayIdAlliance != 0) mountId = it->second.mountDisplayIdAlliance; if (it->second.mountDisplayIdAlliance != 0) mountId = it->second.mountDisplayIdAlliance;
else if (it->second.mountDisplayIdHorde != 0) mountId = it->second.mountDisplayIdHorde; 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; mountId = isAlliance ? 30412u : 30413u;
} }
if (mountId != 0) { 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) { if (taxiClientPath_.size() < 2) {
LOG_WARNING("Taxi path too short: ", taxiClientPath_.size(), " waypoints"); LOG_WARNING("Taxi path too short: ", taxiClientPath_.size(), " waypoints");
return; return;
} }
// Set initial orientation to face the first flight segment // Set initial orientation to face the first non-degenerate flight segment.
if (!entityManager.hasEntity(playerGuid)) return; 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); auto playerEntity = entityManager.getEntity(playerGuid);
if (playerEntity) { 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); playerEntity->setPosition(start.x, start.y, start.z, initialOrientation);
movementInfo.orientation = initialOrientation; }
// Update mount rotation immediately with pitch and roll if (taxiOrientationCallback_) {
if (taxiOrientationCallback_) { taxiOrientationCallback_(initialRenderYaw, initialPitch, initialRoll);
taxiOrientationCallback_(initialOrientation, initialPitch, initialRoll);
}
} }
LOG_INFO("Taxi flight started with ", taxiClientPath_.size(), " spline waypoints"); 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) { void GameHandler::updateClientTaxi(float deltaTime) {
if (!taxiClientActive_ || taxiClientPath_.size() < 2) return; if (!taxiClientActive_ || taxiClientPath_.size() < 2) return;
if (!entityManager.hasEntity(playerGuid)) return;
auto playerEntity = entityManager.getEntity(playerGuid); auto playerEntity = entityManager.getEntity(playerGuid);
if (!playerEntity) return;
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { auto finishTaxiFlight = [&]() {
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()) {
taxiClientActive_ = false; taxiClientActive_ = false;
onTaxiFlight_ = false; onTaxiFlight_ = false;
taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering
@ -6201,10 +6237,50 @@ void GameHandler::updateClientTaxi(float deltaTime) {
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
} }
LOG_INFO("Taxi flight landed (client path)"); LOG_INFO("Taxi flight landed (client path)");
} };
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) {
finishTaxiFlight();
return; 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 // Use Catmull-Rom spline for smooth interpolation between waypoints
// Get surrounding points for spline curve // Get surrounding points for spline curve
glm::vec3 p0 = (taxiClientIndex_ > 0) ? taxiClientPath_[taxiClientIndex_ - 1] : start; glm::vec3 p0 = (taxiClientIndex_ > 0) ? taxiClientPath_[taxiClientIndex_ - 1] : start;
@ -6240,7 +6316,7 @@ void GameHandler::updateClientTaxi(float deltaTime) {
} }
// Calculate yaw from horizontal direction // 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) // Calculate pitch from vertical component (altitude change)
glm::vec3 tangentNorm = tangent / std::max(tangentLen, 0.0001f); glm::vec3 tangentNorm = tangent / std::max(tangentLen, 0.0001f);
@ -6259,15 +6335,20 @@ void GameHandler::updateClientTaxi(float deltaTime) {
// Smooth rotation transition (lerp towards target) // Smooth rotation transition (lerp towards target)
float smoothOrientation = currentOrientation + orientDiff * std::min(1.0f, deltaTime * 3.0f); 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.x = nextPos.x;
movementInfo.y = nextPos.y; movementInfo.y = nextPos.y;
movementInfo.z = nextPos.z; movementInfo.z = nextPos.z;
movementInfo.orientation = smoothOrientation; 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_) { 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) { 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; onTaxiFlight_ = true;
taxiWindowOpen_ = false; taxiWindowOpen_ = false;
taxiMountActive_ = false;
taxiMountDisplayId_ = 0;
taxiActivatePending_ = false; taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f; taxiActivateTimer_ = 0.0f;
applyTaxiMountForCurrentNode(); applyTaxiMountForCurrentNode();
if (socket) {
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
}
LOG_INFO("Taxi flight started!"); LOG_INFO("Taxi flight started!");
} else { } else {
LOG_WARNING("Taxi activation failed, result=", data.result); LOG_WARNING("Taxi activation failed, result=", data.result);
@ -6438,6 +6525,9 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
onTaxiFlight_ = true; onTaxiFlight_ = true;
applyTaxiMountForCurrentNode(); applyTaxiMountForCurrentNode();
} }
if (socket) {
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
}
// Trigger terrain precache immediately (non-blocking). // Trigger terrain precache immediately (non-blocking).
if (taxiPrecacheCallback_) { if (taxiPrecacheCallback_) {

View file

@ -26,10 +26,11 @@ bool AssetManager::initialize(const std::string& dataPath_) {
return false; 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(); auto& memMonitor = core::MemoryMonitor::getInstance();
size_t recommendedBudget = memMonitor.getRecommendedCacheBudget(); 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; initialized = true;
LOG_INFO("Asset manager initialized (dynamic file cache: ", 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::string normalized = normalizePath(path);
std::lock_guard<std::mutex> lock(readMutex); {
std::lock_guard<std::mutex> cacheLock(cacheMutex);
// Check cache first // Check cache first
auto it = fileCache.find(normalized); auto it = fileCache.find(normalized);
if (it != fileCache.end()) { if (it != fileCache.end()) {
// Cache hit - update access time and return cached data // Cache hit - update access time and return cached data
it->second.lastAccessTime = ++fileCacheAccessCounter; it->second.lastAccessTime = ++fileCacheAccessCounter;
fileCacheHits++; fileCacheHits++;
return it->second.data; return it->second.data;
}
fileCacheMisses++;
} }
// Cache miss - decompress from MPQ // Cache miss - decompress from MPQ.
fileCacheMisses++; // Keep MPQ reads serialized, but do not block cache-hit readers on this mutex.
std::vector<uint8_t> data = mpqManager.readFile(normalized); std::vector<uint8_t> data;
{
std::lock_guard<std::mutex> readLock(readMutex);
data = mpqManager.readFile(normalized);
}
if (data.empty()) { if (data.empty()) {
return data; // File not found 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 // Add to cache if within budget
size_t fileSize = data.size(); size_t fileSize = data.size();
if (fileSize > 0 && fileSize < fileCacheBudget / 2) { // Don't cache files > 50% of budget (very aggressive) 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) // Evict old entries if needed (LRU)
while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) { while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) {
// Find least recently used entry // Find least recently used entry
@ -198,7 +206,7 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
} }
void AssetManager::clearCache() { void AssetManager::clearCache() {
std::lock_guard<std::mutex> lock(readMutex); std::scoped_lock lock(readMutex, cacheMutex);
dbcCache.clear(); dbcCache.clear();
fileCache.clear(); fileCache.clear();
fileCacheTotalBytes = 0; fileCacheTotalBytes = 0;
@ -211,6 +219,9 @@ std::string AssetManager::normalizePath(const std::string& path) const {
// Convert forward slashes to backslashes (WoW uses backslashes) // Convert forward slashes to backslashes (WoW uses backslashes)
std::replace(normalized.begin(), normalized.end(), '/', '\\'); 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; return normalized;
} }

View file

@ -112,17 +112,12 @@ void CameraController::update(float deltaTime) {
return; 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_) { if (externalFollow_) {
// Mouse look (right mouse button) camera->setRotation(yaw, pitch);
if (rightMouseDown) { float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime);
int mouseDX, mouseDY; currentDistance += (userTargetDistance - currentDistance) * zoomLerp;
SDL_GetRelativeMouseState(&mouseDX, &mouseDY); collisionDistance = currentDistance;
yaw -= mouseDX * mouseSensitivity;
pitch -= mouseDY * mouseSensitivity;
pitch = glm::clamp(pitch, -89.0f, 89.0f);
camera->setRotation(yaw, pitch);
}
// Position camera behind character during taxi // Position camera behind character during taxi
if (thirdPerson && followTarget) { if (thirdPerson && followTarget) {

View file

@ -125,12 +125,9 @@ void Celestial::shutdown() {
void Celestial::render(const Camera& camera, float timeOfDay, void Celestial::render(const Camera& camera, float timeOfDay,
const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) { const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) {
if (!renderingEnabled || vao == 0 || !celestialShader) { if (!renderingEnabled || vao == 0 || !celestialShader) {
LOG_WARNING("Celestial render blocked: enabled=", renderingEnabled, " vao=", vao, " shader=", (celestialShader ? "ok" : "null"));
return; return;
} }
LOG_INFO("Celestial render: timeOfDay=", timeOfDay, " gameTime=", gameTime);
// Update moon phases from game time if available (deterministic) // Update moon phases from game time if available (deterministic)
if (gameTime >= 0.0f) { if (gameTime >= 0.0f) {
updatePhasesFromGameTime(gameTime); updatePhasesFromGameTime(gameTime);
@ -166,22 +163,16 @@ void Celestial::renderSun(const Camera& camera, float timeOfDay,
const glm::vec3* sunDir, const glm::vec3* sunColor) { const glm::vec3* sunDir, const glm::vec3* sunColor) {
// Sun visible from 5:00 to 19:00 // Sun visible from 5:00 to 19:00
if (timeOfDay < 5.0f || timeOfDay >= 19.0f) { if (timeOfDay < 5.0f || timeOfDay >= 19.0f) {
LOG_INFO("Sun not visible: timeOfDay=", timeOfDay, " (visible 5:00-19:00)");
return; return;
} }
LOG_INFO("Rendering sun: timeOfDay=", timeOfDay, " sunDir=", (sunDir ? "yes" : "no"), " sunColor=", (sunColor ? "yes" : "no"));
celestialShader->use(); celestialShader->use();
// TESTING: Try X-up (final axis test) glm::vec3 dir = sunDir ? glm::normalize(*sunDir) : glm::vec3(0.0f, 0.0f, 1.0f);
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, ")");
// Place sun on sky sphere at fixed distance // Place sun on sky sphere at fixed distance
const float sunDistance = 800.0f; const float sunDistance = 800.0f;
glm::vec3 sunPos = dir * sunDistance; glm::vec3 sunPos = dir * sunDistance;
LOG_INFO("Sun position: dir * ", sunDistance, " = (", sunPos.x, ",", sunPos.y, ",", sunPos.z, ")");
// Create model matrix // Create model matrix
glm::mat4 model = glm::mat4(1.0f); glm::mat4 model = glm::mat4(1.0f);

View file

@ -957,13 +957,9 @@ void Renderer::updateCharacterAnimation() {
// Sync mount instance position and rotation // Sync mount instance position and rotation
float mountBob = 0.0f; float mountBob = 0.0f;
float mountYawRad = glm::radians(characterYaw);
if (mountInstanceId_ > 0) { if (mountInstanceId_ > 0) {
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); 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) // Procedural lean into turns (ground mounts only, optional enhancement)
if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) {
@ -982,7 +978,7 @@ void Renderer::updateCharacterAnimation() {
} }
// Apply pitch (up/down), roll (banking), and yaw for realistic flight // 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 // Drive mount model animation: idle when still, run when moving
auto pickMountAnim = [&](std::initializer_list<uint32_t> candidates, uint32_t fallback) -> uint32_t { 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; glm::mat4 mountSeatTransform;
bool haveSeat = false; bool haveSeat = false;
if (mountSeatAttachmentId_ >= 0) { 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 { glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const {
// TESTING: X-up test glm::vec3 dir = glm::normalize(params.directionalDir);
glm::vec3 dir = glm::vec3(1.0f, 0.0f, 0.0f); // X-up if (glm::length(dir) < 0.0001f) {
glm::vec3 pos = dir * 800.0f; dir = glm::vec3(0.0f, 0.0f, 1.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 pos = dir * 800.0f;
return pos; return pos;
} }

View file

@ -115,9 +115,10 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer*
return false; 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(); auto& memMonitor = core::MemoryMonitor::getInstance();
tileCacheBudgetBytes_ = memMonitor.getRecommendedCacheBudget() / 2; tileCacheBudgetBytes_ = memMonitor.getRecommendedCacheBudget() / 4;
LOG_INFO("Terrain tile cache budget: ", tileCacheBudgetBytes_ / (1024 * 1024), " MB (dynamic)"); LOG_INFO("Terrain tile cache budget: ", tileCacheBudgetBytes_ / (1024 * 1024), " MB (dynamic)");
// Start background worker pool (dynamic: scales with available cores) // 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); std::lock_guard<std::mutex> lock(queueMutex);
loadQueue.push(coord); loadQueue.push_back(coord);
pendingTiles[coord] = true; pendingTiles[coord] = true;
} }
queueCV.notify_all(); queueCV.notify_all();
@ -791,7 +792,7 @@ void TerrainManager::workerLoop() {
if (!loadQueue.empty()) { if (!loadQueue.empty()) {
coord = loadQueue.front(); coord = loadQueue.front();
loadQueue.pop(); loadQueue.pop_front();
hasWork = true; hasWork = true;
} }
} }
@ -1056,7 +1057,7 @@ void TerrainManager::unloadAll() {
// Clear queues // Clear queues
{ {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
while (!loadQueue.empty()) loadQueue.pop(); while (!loadQueue.empty()) loadQueue.pop_front();
while (!readyQueue.empty()) readyQueue.pop(); while (!readyQueue.empty()) readyQueue.pop();
} }
pendingTiles.clear(); pendingTiles.clear();
@ -1353,7 +1354,7 @@ void TerrainManager::streamTiles() {
if (pendingTiles.find(coord) != pendingTiles.end()) continue; if (pendingTiles.find(coord) != pendingTiles.end()) continue;
if (failedTiles.find(coord) != failedTiles.end()) continue; if (failedTiles.find(coord) != failedTiles.end()) continue;
loadQueue.push(coord); loadQueue.push_back(coord);
pendingTiles[coord] = true; 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 (pendingTiles.find(coord) != pendingTiles.end()) continue;
if (failedTiles.find(coord) != failedTiles.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; pendingTiles[coord] = true;
} }