Fix taxi state sync and transport authority; reduce runtime log overhead; restore first-person self-hide

This commit is contained in:
Kelsi 2026-02-11 22:27:02 -08:00
parent 74062aa25f
commit 8bf63b1f06
29 changed files with 529 additions and 360 deletions

View file

@ -65,7 +65,7 @@ private:
struct ActiveSound { struct ActiveSound {
ma_sound* sound; ma_sound* sound;
void* buffer; // ma_audio_buffer* - Keep audio buffer alive void* buffer; // ma_audio_buffer* - Keep audio buffer alive
std::vector<uint8_t> pcmData; // Keep PCM data alive std::shared_ptr<const std::vector<uint8_t>> pcmDataRef; // Keep decoded PCM alive
}; };
std::vector<ActiveSound> activeSounds_; std::vector<ActiveSound> activeSounds_;

View file

@ -112,7 +112,6 @@ private:
bool npcsSpawned = false; bool npcsSpawned = false;
bool spawnSnapToGround = true; bool spawnSnapToGround = true;
float lastFrameTime = 0.0f; float lastFrameTime = 0.0f;
float movementHeartbeatTimer = 0.0f;
// Player character info (for model spawning) // Player character info (for model spawning)
game::Race playerRace_ = game::Race::HUMAN; game::Race playerRace_ = game::Race::HUMAN;

View file

@ -1103,6 +1103,7 @@ private:
std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper
void buildTaxiCostMap(); void buildTaxiCostMap();
void applyTaxiMountForCurrentNode(); void applyTaxiMountForCurrentNode();
void sanitizeMovementForTaxi();
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes); void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
void updateClientTaxi(float deltaTime); void updateClientTaxi(float deltaTime);

View file

@ -5,6 +5,8 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <map> #include <map>
#include <unordered_set>
#include <mutex>
// Forward declare StormLib handle // Forward declare StormLib handle
typedef void* HANDLE; typedef void* HANDLE;
@ -103,6 +105,11 @@ private:
* @param locale Locale string (e.g., "enUS") * @param locale Locale string (e.g., "enUS")
*/ */
bool loadLocaleArchives(const std::string& locale); bool loadLocaleArchives(const std::string& locale);
void logMissingFileOnce(const std::string& filename) const;
mutable std::mutex missingFileMutex_;
mutable std::unordered_set<std::string> missingFileWarnings_;
}; };
} // namespace pipeline } // namespace pipeline

View file

@ -64,6 +64,7 @@ public:
float getPitch() const { return pitch; } float getPitch() const { return pitch; }
float getFacingYaw() const { return facingYaw; } float getFacingYaw() const { return facingYaw; }
bool isThirdPerson() const { return thirdPerson; } bool isThirdPerson() const { return thirdPerson; }
bool isFirstPersonView() const { return thirdPerson && (userTargetDistance <= MIN_DISTANCE + 0.15f); }
bool isGrounded() const { return grounded; } bool isGrounded() const { return grounded; }
bool isJumping() const { return !grounded && verticalVelocity > 0.0f; } bool isJumping() const { return !grounded && verticalVelocity > 0.0f; }
bool isFalling() const { return !grounded && verticalVelocity <= 0.0f; } bool isFalling() const { return !grounded && verticalVelocity <= 0.0f; }

View file

@ -264,6 +264,10 @@ public:
* @param instanceId Instance ID returned by createInstance() * @param instanceId Instance ID returned by createInstance()
*/ */
void removeInstance(uint32_t instanceId); void removeInstance(uint32_t instanceId);
/**
* Remove multiple instances with one spatial-index rebuild.
*/
void removeInstances(const std::vector<uint32_t>& instanceIds);
/** /**
* Clear all models and instances * Clear all models and instances

View file

@ -243,6 +243,7 @@ private:
std::string currentZoneName; std::string currentZoneName;
bool inTavern_ = false; bool inTavern_ = false;
bool inBlacksmith_ = false; bool inBlacksmith_ = false;
float musicSwitchCooldown_ = 0.0f;
// Third-person character state // Third-person character state
glm::vec3 characterPosition = glm::vec3(0.0f); glm::vec3 characterPosition = glm::vec3(0.0f);

View file

@ -319,10 +319,13 @@ private:
std::shared_ptr<PendingTile> getCachedTile(const TileCoord& coord); std::shared_ptr<PendingTile> getCachedTile(const TileCoord& coord);
void putCachedTile(const std::shared_ptr<PendingTile>& tile); void putCachedTile(const std::shared_ptr<PendingTile>& tile);
size_t estimatePendingTileBytes(const PendingTile& tile) const; size_t estimatePendingTileBytes(const PendingTile& tile) const;
void logMissingAdtOnce(const std::string& adtPath);
std::atomic<bool> workerRunning{false}; std::atomic<bool> workerRunning{false};
// Track tiles currently queued or being processed to avoid duplicates // Track tiles currently queued or being processed to avoid duplicates
std::unordered_map<TileCoord, bool, TileCoord::Hash> pendingTiles; std::unordered_map<TileCoord, bool, TileCoord::Hash> pendingTiles;
std::unordered_set<std::string> missingAdtWarnings_;
std::mutex missingAdtWarningsMutex_;
// Dedup set for doodad placements across tile boundaries // Dedup set for doodad placements across tile boundaries
std::unordered_set<uint32_t> placedDoodadIds; std::unordered_set<uint32_t> placedDoodadIds;

View file

@ -207,6 +207,8 @@ private:
// Default white texture (fallback) // Default white texture (fallback)
GLuint whiteTexture = 0; GLuint whiteTexture = 0;
// Opaque alpha fallback for missing/invalid layer alpha maps
GLuint opaqueAlphaTexture = 0;
// Shadow mapping (receiving) // Shadow mapping (receiving)
GLuint shadowDepthTex = 0; GLuint shadowDepthTex = 0;

View file

@ -121,6 +121,10 @@ public:
* @param instanceId Instance to remove * @param instanceId Instance to remove
*/ */
void removeInstance(uint32_t instanceId); void removeInstance(uint32_t instanceId);
/**
* Remove multiple WMO instances with a single spatial-index rebuild.
*/
void removeInstances(const std::vector<uint32_t>& instanceIds);
/** /**
* Remove all instances * Remove all instances

View file

@ -6,10 +6,87 @@
#include "../../extern/miniaudio.h" #include "../../extern/miniaudio.h"
#include <cstring> #include <cstring>
#include <memory>
#include <unordered_map>
namespace wowee { namespace wowee {
namespace audio { namespace audio {
namespace {
struct DecodedWavCacheEntry {
ma_format format = ma_format_unknown;
ma_uint32 channels = 0;
ma_uint32 sampleRate = 0;
ma_uint64 frames = 0;
std::shared_ptr<std::vector<uint8_t>> pcmData;
};
static std::unordered_map<uint64_t, DecodedWavCacheEntry> gDecodedWavCache;
static uint64_t makeWavCacheKey(const std::vector<uint8_t>& wavData) {
uint64_t ptr = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(wavData.data()));
uint64_t sz = static_cast<uint64_t>(wavData.size());
return (ptr * 11400714819323198485ull) ^ (sz + 0x9e3779b97f4a7c15ull + (ptr << 6) + (ptr >> 2));
}
static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCacheEntry& out) {
if (wavData.empty()) return false;
const uint64_t key = makeWavCacheKey(wavData);
if (auto it = gDecodedWavCache.find(key); it != gDecodedWavCache.end()) {
out = it->second;
return true;
}
ma_decoder decoder;
ma_decoder_config decoderConfig = ma_decoder_config_init_default();
ma_result result = ma_decoder_init_memory(
wavData.data(),
wavData.size(),
&decoderConfig,
&decoder
);
if (result != MA_SUCCESS) {
LOG_ERROR("AudioEngine: Failed to decode WAV data (", wavData.size(), " bytes): error ", result);
return false;
}
ma_uint64 totalFrames = 0;
result = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
if (result != MA_SUCCESS) totalFrames = 0;
ma_format format = decoder.outputFormat;
ma_uint32 channels = decoder.outputChannels;
ma_uint32 sampleRate = decoder.outputSampleRate;
ma_uint64 maxFrames = sampleRate * 60;
if (totalFrames == 0 || totalFrames > maxFrames) totalFrames = maxFrames;
size_t bufferSize = totalFrames * channels * ma_get_bytes_per_sample(format);
auto pcmData = std::make_shared<std::vector<uint8_t>>(bufferSize);
ma_uint64 framesRead = 0;
result = ma_decoder_read_pcm_frames(&decoder, pcmData->data(), totalFrames, &framesRead);
ma_decoder_uninit(&decoder);
if (result != MA_SUCCESS || framesRead == 0) {
LOG_ERROR("AudioEngine: Failed to read frames from WAV: error ", result, ", framesRead=", framesRead);
return false;
}
pcmData->resize(framesRead * channels * ma_get_bytes_per_sample(format));
DecodedWavCacheEntry entry;
entry.format = format;
entry.channels = channels;
entry.sampleRate = sampleRate;
entry.frames = framesRead;
entry.pcmData = pcmData;
gDecodedWavCache.emplace(key, entry);
out = entry;
return true;
}
} // namespace
AudioEngine& AudioEngine::instance() { AudioEngine& AudioEngine::instance() {
static AudioEngine instance; static AudioEngine instance;
return instance; return instance;
@ -121,76 +198,26 @@ void AudioEngine::setListenerOrientation(const glm::vec3& forward, const glm::ve
} }
bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume, float pitch) { bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume, float pitch) {
if (!initialized_ || !engine_ || wavData.empty()) { (void)pitch;
if (!initialized_ || !engine_ || wavData.empty()) return false;
DecodedWavCacheEntry decoded;
if (!decodeWavCached(wavData, decoded) || !decoded.pcmData || decoded.frames == 0) {
return false; return false;
} }
// Decode the WAV data first to get PCM format
ma_decoder decoder;
ma_decoder_config decoderConfig = ma_decoder_config_init_default();
ma_result result = ma_decoder_init_memory(
wavData.data(),
wavData.size(),
&decoderConfig,
&decoder
);
if (result != MA_SUCCESS) {
LOG_ERROR("AudioEngine: Failed to decode WAV data (", wavData.size(), " bytes): error ", result);
return false;
}
// Get decoder format info
ma_format format = decoder.outputFormat;
ma_uint32 channels = decoder.outputChannels;
ma_uint32 sampleRate = decoder.outputSampleRate;
// Calculate total frame count
ma_uint64 totalFrames;
result = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
if (result != MA_SUCCESS) {
totalFrames = 0; // Unknown length, will decode what we can
}
// Allocate buffer for decoded PCM data (limit to 60 seconds max for ambient loops)
ma_uint64 maxFrames = sampleRate * 60;
if (totalFrames == 0 || totalFrames > maxFrames) {
totalFrames = maxFrames;
}
size_t bufferSize = totalFrames * channels * ma_get_bytes_per_sample(format);
std::vector<uint8_t> pcmData(bufferSize);
// Decode all frames
ma_uint64 framesRead = 0;
result = ma_decoder_read_pcm_frames(&decoder, pcmData.data(), totalFrames, &framesRead);
ma_decoder_uninit(&decoder);
if (result != MA_SUCCESS || framesRead == 0) {
LOG_ERROR("AudioEngine: Failed to read frames from WAV: error ", result, ", framesRead=", framesRead);
return false;
}
// Only log for large files (>1MB)
if (wavData.size() > 1000000) {
LOG_INFO("AudioEngine: Decoded ", framesRead, " frames (", framesRead / (float)sampleRate, "s) from ", wavData.size(), " byte WAV");
}
// Resize pcmData to actual size used
pcmData.resize(framesRead * channels * ma_get_bytes_per_sample(format));
// Create audio buffer from decoded PCM data (heap allocated to keep alive) // Create audio buffer from decoded PCM data (heap allocated to keep alive)
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init( ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(
format, decoded.format,
channels, decoded.channels,
framesRead, decoded.frames,
pcmData.data(), decoded.pcmData->data(),
nullptr // No custom allocator nullptr // No custom allocator
); );
bufferConfig.sampleRate = sampleRate; // Critical: preserve original sample rate! bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate!
ma_audio_buffer* audioBuffer = new ma_audio_buffer(); ma_audio_buffer* audioBuffer = new ma_audio_buffer();
result = ma_audio_buffer_init(&bufferConfig, audioBuffer); ma_result result = ma_audio_buffer_init(&bufferConfig, audioBuffer);
if (result != MA_SUCCESS) { if (result != MA_SUCCESS) {
LOG_WARNING("Failed to create audio buffer: ", result); LOG_WARNING("Failed to create audio buffer: ", result);
delete audioBuffer; delete audioBuffer;
@ -229,8 +256,8 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume,
return false; return false;
} }
// Track this sound for cleanup (move pcmData to keep it alive) // Track this sound for cleanup (decoded PCM shared across plays)
activeSounds_.push_back({sound, audioBuffer, std::move(pcmData)}); activeSounds_.push_back({sound, audioBuffer, decoded.pcmData});
return true; return true;
} }
@ -244,68 +271,29 @@ bool AudioEngine::playSound2D(const std::string& mpqPath, float volume, float pi
bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position, bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position,
float volume, float pitch, float maxDistance) { float volume, float pitch, float maxDistance) {
if (!initialized_ || !engine_ || wavData.empty()) { if (!initialized_ || !engine_ || wavData.empty()) return false;
DecodedWavCacheEntry decoded;
if (!decodeWavCached(wavData, decoded) || !decoded.pcmData || decoded.frames == 0) {
return false; return false;
} }
// Decode WAV data first LOG_DEBUG("playSound3D: cached WAV - format:", decoded.format,
ma_decoder decoder; " channels:", decoded.channels, " sampleRate:", decoded.sampleRate,
ma_decoder_config decoderConfig = ma_decoder_config_init_default(); " pitch:", pitch);
ma_result result = ma_decoder_init_memory(
wavData.data(),
wavData.size(),
&decoderConfig,
&decoder
);
if (result != MA_SUCCESS) {
LOG_WARNING("playSound3D: Failed to decode WAV, error: ", result);
return false;
}
ma_format format = decoder.outputFormat;
ma_uint32 channels = decoder.outputChannels;
ma_uint32 sampleRate = decoder.outputSampleRate;
LOG_DEBUG("playSound3D: Decoded WAV - format:", format, " channels:", channels, " sampleRate:", sampleRate, " pitch:", pitch);
ma_uint64 totalFrames;
result = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
if (result != MA_SUCCESS) {
totalFrames = 0;
}
// Limit to 60 seconds max for ambient loops (same as 2D)
ma_uint64 maxFrames = sampleRate * 60;
if (totalFrames == 0 || totalFrames > maxFrames) {
totalFrames = maxFrames;
}
size_t bufferSize = totalFrames * channels * ma_get_bytes_per_sample(format);
std::vector<uint8_t> pcmData(bufferSize);
ma_uint64 framesRead = 0;
result = ma_decoder_read_pcm_frames(&decoder, pcmData.data(), totalFrames, &framesRead);
ma_decoder_uninit(&decoder);
if (result != MA_SUCCESS || framesRead == 0) {
return false;
}
pcmData.resize(framesRead * channels * ma_get_bytes_per_sample(format));
// Create audio buffer with correct sample rate // Create audio buffer with correct sample rate
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init( ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(
format, decoded.format,
channels, decoded.channels,
framesRead, decoded.frames,
pcmData.data(), decoded.pcmData->data(),
nullptr nullptr
); );
bufferConfig.sampleRate = sampleRate; // Critical: preserve original sample rate! bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate!
ma_audio_buffer* audioBuffer = new ma_audio_buffer(); ma_audio_buffer* audioBuffer = new ma_audio_buffer();
result = ma_audio_buffer_init(&bufferConfig, audioBuffer); ma_result result = ma_audio_buffer_init(&bufferConfig, audioBuffer);
if (result != MA_SUCCESS) { if (result != MA_SUCCESS) {
delete audioBuffer; delete audioBuffer;
return false; return false;
@ -350,7 +338,7 @@ bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::ve
} }
// Track for cleanup // Track for cleanup
activeSounds_.push_back({sound, audioBuffer, std::move(pcmData)}); activeSounds_.push_back({sound, audioBuffer, decoded.pcmData});
return true; return true;
} }

View file

@ -58,9 +58,6 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
return; return;
} }
// Stop current playback
AudioEngine::instance().stopMusic();
// Play with AudioEngine (non-blocking, streams from memory) // Play with AudioEngine (non-blocking, streams from memory)
float volume = volumePercent / 100.0f; float volume = volumePercent / 100.0f;
if (AudioEngine::instance().playMusic(cacheIt->second, volume, loop)) { if (AudioEngine::instance().playMusic(cacheIt->second, volume, loop)) {
@ -103,9 +100,6 @@ void MusicManager::playFilePath(const std::string& filePath, bool loop) {
return; return;
} }
// Stop current playback
AudioEngine::instance().stopMusic();
// Play with AudioEngine // Play with AudioEngine
float volume = volumePercent / 100.0f; float volume = volumePercent / 100.0f;
if (AudioEngine::instance().playMusic(data, volume, loop)) { if (AudioEngine::instance().playMusic(data, volume, loop)) {

View file

@ -57,9 +57,10 @@ const char* Application::mapIdToName(uint32_t mapId) {
switch (mapId) { switch (mapId) {
case 0: return "Azeroth"; case 0: return "Azeroth";
case 1: return "Kalimdor"; case 1: return "Kalimdor";
case 369: return "DeeprunTram";
case 530: return "Outland"; case 530: return "Outland";
case 571: return "Northrend"; case 571: return "Northrend";
default: return "Azeroth"; default: return "";
} }
} }
@ -568,11 +569,10 @@ void Application::update(float deltaTime) {
} }
if (renderer && renderer->getTerrainManager()) { if (renderer && renderer->getTerrainManager()) {
renderer->getTerrainManager()->setStreamingEnabled(true); renderer->getTerrainManager()->setStreamingEnabled(true);
// Slightly slower stream tick on taxi reduces bursty I/O and frame hitches. // Keep taxi streaming responsive so flight paths don't outrun terrain/model uploads.
renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.2f : 0.1f); renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.1f : 0.1f);
// Keep taxi streaming focused ahead on the route to reduce burst loads. renderer->getTerrainManager()->setLoadRadius(onTaxi ? 3 : 4);
renderer->getTerrainManager()->setLoadRadius(onTaxi ? 2 : 4); renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 6 : 7);
renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 5 : 7);
renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi); renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi);
} }
lastTaxiFlight_ = onTaxi; lastTaxiFlight_ = onTaxi;
@ -637,17 +637,8 @@ void Application::update(float deltaTime) {
} }
} }
// Send periodic movement heartbeats. // Movement heartbeat is sent from GameHandler::update() to avoid
// Keep them active during taxi as well to avoid occasional server-side // duplicate packets from multiple update loops.
// flight stalls waiting for movement sync updates.
if (gameHandler && renderer) {
movementHeartbeatTimer += deltaTime;
float hbInterval = onTaxi ? 0.25f : 0.5f;
if (movementHeartbeatTimer >= hbInterval) {
movementHeartbeatTimer = 0.0f;
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT);
}
}
auto sync2 = std::chrono::high_resolution_clock::now(); auto sync2 = std::chrono::high_resolution_clock::now();
syncTime += std::chrono::duration<float, std::milli>(sync2 - sync1).count(); syncTime += std::chrono::duration<float, std::milli>(sync2 - sync1).count();
@ -1048,17 +1039,24 @@ 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.
// Use stride to avoid enqueueing huge numbers of tiles at once. // Denser sampling + neighbor coverage reduces in-flight stream spikes.
const size_t stride = 4; const size_t stride = 2;
for (size_t i = 0; i < path.size(); i += stride) { for (size_t i = 0; i < path.size(); i += stride) {
const auto& waypoint = path[i]; 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));
// Precache only the sampled tile itself; terrain streaming handles neighbors.
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) { if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
uniqueTiles.insert({tileX, tileY}); for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
int nx = tileX + dx;
int ny = tileY + dy;
if (nx >= 0 && nx <= 63 && ny >= 0 && ny <= 63) {
uniqueTiles.insert({nx, ny});
}
}
}
} }
} }
// Ensure final destination tile is included. // Ensure final destination tile is included.
@ -1067,11 +1065,22 @@ void Application::setupUICallbacks() {
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));
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) { if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
uniqueTiles.insert({tileX, tileY}); for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
int nx = tileX + dx;
int ny = tileY + dy;
if (nx >= 0 && nx <= 63 && ny >= 0 && ny <= 63) {
uniqueTiles.insert({nx, ny});
}
}
}
} }
} }
std::vector<std::pair<int, int>> tilesToLoad(uniqueTiles.begin(), uniqueTiles.end()); std::vector<std::pair<int, int>> tilesToLoad(uniqueTiles.begin(), uniqueTiles.end());
if (tilesToLoad.size() > 512) {
tilesToLoad.resize(512);
}
LOG_INFO("Precaching ", tilesToLoad.size(), " tiles for taxi route"); LOG_INFO("Precaching ", tilesToLoad.size(), " tiles for taxi route");
renderer->getTerrainManager()->precacheTiles(tilesToLoad); renderer->getTerrainManager()->precacheTiles(tilesToLoad);
}); });
@ -2051,7 +2060,37 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
showProgress("Entering world...", 0.0f); showProgress("Entering world...", 0.0f);
std::string mapName = mapIdToName(mapId); // Resolve map folder name from Map.dbc (authoritative for world/instance maps).
// This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor.
static bool mapNameCacheLoaded = false;
static std::unordered_map<uint32_t, std::string> mapNameById;
if (!mapNameCacheLoaded && assetManager) {
mapNameCacheLoaded = true;
if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) {
mapNameById.reserve(mapDbc->getRecordCount());
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
uint32_t id = mapDbc->getUInt32(i, 0);
std::string internalName = mapDbc->getString(i, 1);
if (!internalName.empty()) {
mapNameById[id] = std::move(internalName);
}
}
LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById.size(), " entries");
} else {
LOG_WARNING("Map.dbc not available; using fallback map-id mapping");
}
}
std::string mapName;
if (auto it = mapNameById.find(mapId); it != mapNameById.end()) {
mapName = it->second;
} else {
mapName = mapIdToName(mapId);
}
if (mapName.empty()) {
LOG_WARNING("Unknown mapId ", mapId, " (no Map.dbc entry); falling back to Azeroth");
mapName = "Azeroth";
}
LOG_INFO("Loading online world terrain for map '", mapName, "' (ID ", mapId, ")"); LOG_INFO("Loading online world terrain for map '", mapName, "' (ID ", mapId, ")");
// Convert server coordinates to canonical WoW coordinates // Convert server coordinates to canonical WoW coordinates
@ -3091,7 +3130,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Track instance // Track instance
creatureInstances_[guid] = instanceId; creatureInstances_[guid] = instanceId;
creatureModelIds_[guid] = modelId; creatureModelIds_[guid] = modelId;
LOG_INFO("Spawned creature: guid=0x", std::hex, guid, std::dec, LOG_DEBUG("Spawned creature: guid=0x", std::hex, guid, std::dec,
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
} }
@ -3107,7 +3146,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
// Already have a render instance — update its position (e.g. transport re-creation) // Already have a render instance — update its position (e.g. transport re-creation)
auto& info = gameObjectInstances_[guid]; auto& info = gameObjectInstances_[guid];
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
LOG_INFO("GameObject position update: displayId=", displayId, " guid=0x", std::hex, guid, std::dec, LOG_DEBUG("GameObject position update: displayId=", displayId, " guid=0x", std::hex, guid, std::dec,
" pos=(", x, ", ", y, ", ", z, ")"); " pos=(", x, ", ", y, ", ", z, ")");
if (renderer) { if (renderer) {
if (info.isWmo) { if (info.isWmo) {
@ -3157,7 +3196,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
} }
// Log spawns to help debug duplicate objects (e.g., cathedral issue) // Log spawns to help debug duplicate objects (e.g., cathedral issue)
LOG_INFO("GameObject spawn: displayId=", displayId, " guid=0x", std::hex, guid, std::dec, LOG_DEBUG("GameObject spawn: displayId=", displayId, " guid=0x", std::hex, guid, std::dec,
" model=", modelPath, " pos=(", x, ", ", y, ", ", z, ")"); " model=", modelPath, " pos=(", x, ", ", y, ", ", z, ")");
std::string lowerPath = modelPath; std::string lowerPath = modelPath;
@ -3182,7 +3221,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
auto wmoData = assetManager->readFile(modelPath); auto wmoData = assetManager->readFile(modelPath);
if (!wmoData.empty()) { if (!wmoData.empty()) {
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
LOG_INFO("Gameobject WMO root loaded: ", modelPath, " nGroups=", wmoModel.nGroups); LOG_DEBUG("Gameobject WMO root loaded: ", modelPath, " nGroups=", wmoModel.nGroups);
int loadedGroups = 0; int loadedGroups = 0;
if (wmoModel.nGroups > 0) { if (wmoModel.nGroups > 0) {
std::string basePath = modelPath; std::string basePath = modelPath;
@ -3244,7 +3283,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
} }
gameObjectInstances_[guid] = {modelId, instanceId, true}; gameObjectInstances_[guid] = {modelId, instanceId, true};
LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec, LOG_DEBUG("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
// Spawn WMO doodads (chairs, furniture, etc.) as child M2 instances // Spawn WMO doodads (chairs, furniture, etc.) as child M2 instances
@ -3372,7 +3411,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
gameObjectInstances_[guid] = {modelId, instanceId, false}; gameObjectInstances_[guid] = {modelId, instanceId, false};
} }
LOG_INFO("Spawned gameobject: guid=0x", std::hex, guid, std::dec, LOG_DEBUG("Spawned gameobject: guid=0x", std::hex, guid, std::dec,
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
} }
@ -3697,7 +3736,7 @@ void Application::despawnOnlineCreature(uint64_t guid) {
creatureInstances_.erase(it); creatureInstances_.erase(it);
creatureModelIds_.erase(guid); creatureModelIds_.erase(guid);
LOG_INFO("Despawned creature: guid=0x", std::hex, guid, std::dec); LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec);
} }
void Application::despawnOnlineGameObject(uint64_t guid) { void Application::despawnOnlineGameObject(uint64_t guid) {
@ -3718,7 +3757,7 @@ void Application::despawnOnlineGameObject(uint64_t guid) {
gameObjectInstances_.erase(it); gameObjectInstances_.erase(it);
LOG_INFO("Despawned gameobject: guid=0x", std::hex, guid, std::dec); LOG_DEBUG("Despawned gameobject: guid=0x", std::hex, guid, std::dec);
} }
void Application::loadQuestMarkerModels() { void Application::loadQuestMarkerModels() {

View file

@ -18,6 +18,7 @@
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <functional> #include <functional>
#include <cstdlib> #include <cstdlib>
#include <zlib.h> #include <zlib.h>
@ -169,7 +170,8 @@ void GameHandler::update(float deltaTime) {
timeSinceLastPing = 0.0f; timeSinceLastPing = 0.0f;
} }
if (timeSinceLastMoveHeartbeat_ >= moveHeartbeatInterval_) { float heartbeatInterval = (onTaxiFlight_ || taxiActivatePending_ || taxiClientActive_) ? 0.25f : moveHeartbeatInterval_;
if (timeSinceLastMoveHeartbeat_ >= heartbeatInterval) {
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT); sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
timeSinceLastMoveHeartbeat_ = 0.0f; timeSinceLastMoveHeartbeat_ = 0.0f;
} }
@ -311,7 +313,13 @@ void GameHandler::update(float deltaTime) {
if (taxiActivatePending_) { if (taxiActivatePending_) {
taxiActivateTimer_ += deltaTime; taxiActivateTimer_ += deltaTime;
if (!onTaxiFlight_ && taxiActivateTimer_ > 5.0f) { if (taxiActivateTimer_ > 5.0f) {
// If client taxi simulation is already active, server reply may be missing/late.
// Do not cancel the flight in that case; clear pending state and continue.
if (onTaxiFlight_ || taxiClientActive_ || taxiMountActive_) {
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
} else {
taxiActivatePending_ = false; taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f; taxiActivateTimer_ = 0.0f;
if (taxiMountActive_ && mountCallback_) { if (taxiMountActive_ && mountCallback_) {
@ -323,6 +331,7 @@ void GameHandler::update(float deltaTime) {
taxiClientPath_.clear(); taxiClientPath_.clear();
onTaxiFlight_ = false; onTaxiFlight_ = false;
LOG_WARNING("Taxi activation timed out"); LOG_WARNING("Taxi activation timed out");
}
} }
} }
@ -531,7 +540,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
break; break;
case Opcode::SMSG_UPDATE_OBJECT: case Opcode::SMSG_UPDATE_OBJECT:
LOG_INFO("Received SMSG_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize()); LOG_DEBUG("Received SMSG_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize());
// Can be received after entering world // Can be received after entering world
if (state == WorldState::IN_WORLD) { if (state == WorldState::IN_WORLD) {
handleUpdateObject(packet); handleUpdateObject(packet);
@ -539,7 +548,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
break; break;
case Opcode::SMSG_COMPRESSED_UPDATE_OBJECT: case Opcode::SMSG_COMPRESSED_UPDATE_OBJECT:
LOG_INFO("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize()); LOG_DEBUG("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize());
// Compressed version of UPDATE_OBJECT // Compressed version of UPDATE_OBJECT
if (state == WorldState::IN_WORLD) { if (state == WorldState::IN_WORLD) {
handleCompressedUpdateObject(packet); handleCompressedUpdateObject(packet);
@ -1243,7 +1252,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
break; break;
default: default:
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec); // Log each unknown opcode once to avoid log-file I/O spikes in busy areas.
static std::unordered_set<uint16_t> loggedUnhandledOpcodes;
if (loggedUnhandledOpcodes.insert(static_cast<uint16_t>(opcode)).second) {
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
}
break; break;
} }
} }
@ -1755,8 +1768,7 @@ void GameHandler::sendMovement(Opcode opcode) {
(opcode == Opcode::CMSG_MOVE_STOP) || (opcode == Opcode::CMSG_MOVE_STOP) ||
(opcode == Opcode::CMSG_MOVE_STOP_STRAFE) || (opcode == Opcode::CMSG_MOVE_STOP_STRAFE) ||
(opcode == Opcode::CMSG_MOVE_STOP_TURN) || (opcode == Opcode::CMSG_MOVE_STOP_TURN) ||
(opcode == Opcode::CMSG_MOVE_STOP_SWIM) || (opcode == Opcode::CMSG_MOVE_STOP_SWIM);
(opcode == Opcode::CMSG_MOVE_FALL_LAND);
if ((onTaxiFlight_ || taxiMountActive_) && !taxiAllowed) return; if ((onTaxiFlight_ || taxiMountActive_) && !taxiAllowed) return;
if (resurrectPending_ && !taxiAllowed) return; if (resurrectPending_ && !taxiAllowed) return;
@ -1811,6 +1823,10 @@ void GameHandler::sendMovement(Opcode opcode) {
break; break;
} }
if (onTaxiFlight_ || taxiMountActive_ || taxiActivatePending_ || taxiClientActive_) {
sanitizeMovementForTaxi();
}
// Add transport data if player is on a transport // Add transport data if player is on a transport
if (isOnTransport()) { if (isOnTransport()) {
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT); movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
@ -1868,6 +1884,29 @@ void GameHandler::sendMovement(Opcode opcode) {
socket->send(packet); socket->send(packet);
} }
void GameHandler::sanitizeMovementForTaxi() {
constexpr uint32_t kClearTaxiFlags =
static_cast<uint32_t>(MovementFlags::FORWARD) |
static_cast<uint32_t>(MovementFlags::BACKWARD) |
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT) |
static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
static_cast<uint32_t>(MovementFlags::TURN_RIGHT) |
static_cast<uint32_t>(MovementFlags::PITCH_UP) |
static_cast<uint32_t>(MovementFlags::PITCH_DOWN) |
static_cast<uint32_t>(MovementFlags::FALLING) |
static_cast<uint32_t>(MovementFlags::FALLINGFAR) |
static_cast<uint32_t>(MovementFlags::SWIMMING);
movementInfo.flags &= ~kClearTaxiFlags;
movementInfo.fallTime = 0;
movementInfo.jumpVelocity = 0.0f;
movementInfo.jumpSinAngle = 0.0f;
movementInfo.jumpCosAngle = 0.0f;
movementInfo.jumpXYSpeed = 0.0f;
movementInfo.pitch = 0.0f;
}
void GameHandler::forceClearTaxiAndMovementState() { void GameHandler::forceClearTaxiAndMovementState() {
taxiActivatePending_ = false; taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f; taxiActivateTimer_ = 0.0f;
@ -1934,7 +1973,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
continue; continue;
} }
LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec); LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
// Trigger despawn callbacks before removing entity // Trigger despawn callbacks before removing entity
auto entity = entityManager.getEntity(guid); auto entity = entityManager.getEntity(guid);
if (entity) { if (entity) {
@ -2092,6 +2131,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) { if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) {
onTaxiFlight_ = true; onTaxiFlight_ = true;
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f); taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
sanitizeMovementForTaxi();
applyTaxiMountForCurrentNode(); applyTaxiMountForCurrentNode();
} }
} }
@ -2598,7 +2638,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
} }
void GameHandler::handleCompressedUpdateObject(network::Packet& packet) { void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
LOG_INFO("Handling SMSG_COMPRESSED_UPDATE_OBJECT, packet size: ", packet.getSize()); LOG_DEBUG("Handling SMSG_COMPRESSED_UPDATE_OBJECT, packet size: ", packet.getSize());
// First 4 bytes = decompressed size // First 4 bytes = decompressed size
if (packet.getSize() < 4) { if (packet.getSize() < 4) {
@ -2607,7 +2647,7 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
} }
uint32_t decompressedSize = packet.readUInt32(); uint32_t decompressedSize = packet.readUInt32();
LOG_INFO(" Decompressed size: ", decompressedSize); LOG_DEBUG(" Decompressed size: ", decompressedSize);
if (decompressedSize == 0 || decompressedSize > 1024 * 1024) { if (decompressedSize == 0 || decompressedSize > 1024 * 1024) {
LOG_WARNING("Invalid decompressed size: ", decompressedSize); LOG_WARNING("Invalid decompressed size: ", decompressedSize);
@ -6335,6 +6375,7 @@ void GameHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
movementInfo.y = start.y; movementInfo.y = start.y;
movementInfo.z = start.z; movementInfo.z = start.z;
movementInfo.orientation = initialOrientation; movementInfo.orientation = initialOrientation;
sanitizeMovementForTaxi();
auto playerEntity = entityManager.getEntity(playerGuid); auto playerEntity = entityManager.getEntity(playerGuid);
if (playerEntity) { if (playerEntity) {
@ -6495,8 +6536,8 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
} }
// Guard against stray/mis-mapped packets being treated as taxi replies. // Guard against stray/mis-mapped packets being treated as taxi replies.
// Only honor taxi replies when a taxi flow is actually active. // We only consume a reply while an activation request is pending.
if (!taxiActivatePending_ && !taxiWindowOpen_ && !onTaxiFlight_) { if (!taxiActivatePending_) {
LOG_DEBUG("Ignoring stray taxi reply: result=", data.result); LOG_DEBUG("Ignoring stray taxi reply: result=", data.result);
return; return;
} }
@ -6509,6 +6550,7 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
} }
onTaxiFlight_ = true; onTaxiFlight_ = true;
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f); taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
sanitizeMovementForTaxi();
taxiWindowOpen_ = false; taxiWindowOpen_ = false;
taxiActivatePending_ = false; taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f; taxiActivateTimer_ = 0.0f;
@ -6518,6 +6560,13 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
} }
LOG_INFO("Taxi flight started!"); LOG_INFO("Taxi flight started!");
} else { } else {
// If local taxi motion already started, treat late failure as stale and ignore.
if (onTaxiFlight_ || taxiClientActive_) {
LOG_WARNING("Ignoring stale taxi failure reply while flight is active: result=", data.result);
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
return;
}
LOG_WARNING("Taxi activation failed, result=", data.result); LOG_WARNING("Taxi activation failed, result=", data.result);
addSystemChatMessage("Cannot take that flight path."); addSystemChatMessage("Cannot take that flight path.");
taxiActivatePending_ = false; taxiActivatePending_ = false;
@ -6678,6 +6727,7 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
taxiStartGrace_ = 2.0f; taxiStartGrace_ = 2.0f;
if (!onTaxiFlight_) { if (!onTaxiFlight_) {
onTaxiFlight_ = true; onTaxiFlight_ = true;
sanitizeMovementForTaxi();
applyTaxiMountForCurrentNode(); applyTaxiMountForCurrentNode();
} }
if (socket) { if (socket) {
@ -6720,6 +6770,11 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
taxiFlightStartCallback_(); taxiFlightStartCallback_();
} }
startClientTaxiPath(path); startClientTaxiPath(path);
// We run taxi movement locally immediately; don't keep a long-lived pending state.
if (taxiClientActive_) {
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
}
addSystemChatMessage("Flight started."); addSystemChatMessage("Flight started.");

View file

@ -254,84 +254,15 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
} }
pathTimeMs = transport.localClockMs % path.durationMs; pathTimeMs = transport.localClockMs % path.durationMs;
} else { } else {
// Server-driven transport without clock sync. // Server-driven transport without clock sync:
// Do not auto-fallback to local DBC paths; remapped paths can be wrong and cause // stay server-authoritative and never switch to DBC/client animation fallback.
// fast sideways movement, diving below water, or despawn-like behavior. // For short server update gaps, apply lightweight dead-reckoning only when we
// Instead, briefly dead-reckon from recent authoritative velocity to avoid visual stutter // have measured velocity from at least one authoritative delta.
// when update bursts are sparse.
constexpr float kMaxExtrapolationSec = 8.0f; constexpr float kMaxExtrapolationSec = 8.0f;
const float ageSec = elapsedTime_ - transport.lastServerUpdate; const float ageSec = elapsedTime_ - transport.lastServerUpdate;
if (transport.hasServerVelocity && ageSec > 0.0f && ageSec <= kMaxExtrapolationSec) { if (transport.hasServerVelocity && ageSec > 0.0f && ageSec <= kMaxExtrapolationSec) {
const float blend = glm::clamp(1.0f - (ageSec / kMaxExtrapolationSec), 0.0f, 1.0f); const float blend = glm::clamp(1.0f - (ageSec / kMaxExtrapolationSec), 0.0f, 1.0f);
transport.position += transport.serverLinearVelocity * (deltaTime * blend); transport.position += transport.serverLinearVelocity * (deltaTime * blend);
} else if (transport.serverUpdateCount <= 1 &&
ageSec >= 1.0f &&
path.fromDBC && !path.zOnly && path.durationMs > 0 && path.points.size() > 1 &&
((transport.guid & 0xFFF0000000000000ULL) == 0x1FC0000000000000ULL)) {
// Spawn-only fallback: only for world transport GUIDs (0x1fc...), not all transport-like objects.
glm::vec3 localTarget = transport.position - transport.basePosition;
uint32_t bestTimeMs = 0;
float bestScore = FLT_MAX;
float bestD2 = FLT_MAX;
constexpr uint32_t samples = 600;
for (uint32_t i = 0; i < samples; ++i) {
uint32_t t = static_cast<uint32_t>((static_cast<uint64_t>(i) * path.durationMs) / samples);
glm::vec3 off = evalTimedCatmullRom(path, t);
glm::vec3 d = off - localTarget;
float d2 = glm::dot(d, d);
float score = d2;
if (transport.hasServerYaw) {
constexpr uint32_t kHeadingDtMs = 250;
uint32_t tNext = (t + kHeadingDtMs) % path.durationMs;
glm::vec3 offNext = evalTimedCatmullRom(path, tNext);
glm::vec3 tangent = offNext - off;
if (glm::length2(tangent) > 1e-6f) {
float yaw = std::atan2(tangent.y, tangent.x);
float dyaw = yaw - transport.serverYaw;
while (dyaw > glm::pi<float>()) dyaw -= glm::two_pi<float>();
while (dyaw < -glm::pi<float>()) dyaw += glm::two_pi<float>();
constexpr float kHeadingWeight = 60.0f;
score += (kHeadingWeight * std::abs(dyaw)) * (kHeadingWeight * std::abs(dyaw));
}
}
if (score < bestScore) {
bestScore = score;
bestD2 = d2;
bestTimeMs = t;
}
}
constexpr float kMaxPhaseDrift = 120.0f;
if (bestD2 <= (kMaxPhaseDrift * kMaxPhaseDrift)) {
bool reverse = false;
if (transport.hasServerYaw) {
constexpr uint32_t kYawDtMs = 250;
uint32_t tNext = (bestTimeMs + kYawDtMs) % path.durationMs;
glm::vec3 p0 = evalTimedCatmullRom(path, bestTimeMs);
glm::vec3 p1 = evalTimedCatmullRom(path, tNext);
glm::vec3 d = p1 - p0;
if (glm::length2(d) > 1e-6f) {
float yawFwd = std::atan2(d.y, d.x);
float yawRev = yawFwd + glm::pi<float>();
auto angleDiff = [](float a, float b) -> float {
float d = a - b;
while (d > glm::pi<float>()) d -= glm::two_pi<float>();
while (d < -glm::pi<float>()) d += glm::two_pi<float>();
return std::abs(d);
};
reverse = angleDiff(yawRev, transport.serverYaw) < angleDiff(yawFwd, transport.serverYaw);
}
}
transport.useClientAnimation = true;
transport.localClockMs = bestTimeMs;
transport.clientAnimationReverse = reverse;
LOG_WARNING("TransportManager: No follow-up server updates for world transport 0x", std::hex, transport.guid, std::dec,
" (", ageSec, "s since spawn); enabling guarded fallback at t=", bestTimeMs,
"ms (phaseDrift=", std::sqrt(bestD2), ", reverse=", reverse, ")");
}
} }
updateTransformMatrices(transport); updateTransformMatrices(transport);

View file

@ -1683,7 +1683,7 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
// Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.) // Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.)
// We've got what we need for display purposes // We've got what we need for display purposes
LOG_INFO("Creature query response: ", data.name, " (type=", data.creatureType, LOG_DEBUG("Creature query response: ", data.name, " (type=", data.creatureType,
" rank=", data.rank, ")"); " rank=", data.rank, ")");
return true; return true;
} }
@ -1718,7 +1718,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue
packet.readString(); packet.readString();
packet.readString(); packet.readString();
LOG_INFO("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")"); LOG_DEBUG("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
return true; return true;
} }

View file

@ -45,7 +45,7 @@ ADTTerrain ADTLoader::load(const std::vector<uint8_t>& adtData) {
return terrain; return terrain;
} }
LOG_INFO("Loading ADT terrain (", adtData.size(), " bytes)"); LOG_DEBUG("Loading ADT terrain (", adtData.size(), " bytes)");
size_t offset = 0; size_t offset = 0;
int chunkIndex = 0; int chunkIndex = 0;
@ -88,7 +88,7 @@ ADTTerrain ADTLoader::load(const std::vector<uint8_t>& adtData) {
parseMODF(chunkData, chunkSize, terrain); parseMODF(chunkData, chunkSize, terrain);
} }
else if (header.magic == MH2O) { else if (header.magic == MH2O) {
LOG_INFO("Found MH2O chunk (", chunkSize, " bytes)"); LOG_DEBUG("Found MH2O chunk (", chunkSize, " bytes)");
parseMH2O(chunkData, chunkSize, terrain); parseMH2O(chunkData, chunkSize, terrain);
} }
else if (header.magic == MCNK) { else if (header.magic == MCNK) {
@ -204,13 +204,13 @@ void ADTLoader::parseMWMO(const uint8_t* data, size_t size, ADTTerrain& terrain)
offset += nameLen + 1; offset += nameLen + 1;
} }
LOG_INFO("Loaded ", terrain.wmoNames.size(), " WMO names from MWMO chunk"); LOG_DEBUG("Loaded ", terrain.wmoNames.size(), " WMO names from MWMO chunk");
for (size_t i = 0; i < terrain.wmoNames.size(); i++) { for (size_t i = 0; i < terrain.wmoNames.size(); i++) {
LOG_INFO(" WMO[", i, "]: ", terrain.wmoNames[i]); LOG_DEBUG(" WMO[", i, "]: ", terrain.wmoNames[i]);
// Flag potential duplicate cathedral models // Flag potential duplicate cathedral models
if (terrain.wmoNames[i].find("cathedral") != std::string::npos || if (terrain.wmoNames[i].find("cathedral") != std::string::npos ||
terrain.wmoNames[i].find("Cathedral") != std::string::npos) { terrain.wmoNames[i].find("Cathedral") != std::string::npos) {
LOG_INFO("*** CATHEDRAL WMO FOUND: ", terrain.wmoNames[i]); LOG_DEBUG("*** CATHEDRAL WMO FOUND: ", terrain.wmoNames[i]);
} }
} }
} }
@ -238,7 +238,7 @@ void ADTLoader::parseMDDF(const uint8_t* data, size_t size, ADTTerrain& terrain)
terrain.doodadPlacements.push_back(placement); terrain.doodadPlacements.push_back(placement);
} }
LOG_INFO("Loaded ", terrain.doodadPlacements.size(), " doodad placements"); LOG_DEBUG("Loaded ", terrain.doodadPlacements.size(), " doodad placements");
} }
void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain) { void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain) {
@ -276,7 +276,7 @@ void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain)
std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::toupper); std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::toupper);
if (upperName.find("STORMWIND.WMO") != std::string::npos) { if (upperName.find("STORMWIND.WMO") != std::string::npos) {
LOG_INFO("*** STORMWIND.WMO PLACEMENT:", LOG_DEBUG("*** STORMWIND.WMO PLACEMENT:",
" uniqueId=", placement.uniqueId, " uniqueId=", placement.uniqueId,
" pos=(", placement.position[0], ", ", placement.position[1], ", ", placement.position[2], ")", " pos=(", placement.position[0], ", ", placement.position[1], ", ", placement.position[2], ")",
" rot=(", placement.rotation[0], ", ", placement.rotation[1], ", ", placement.rotation[2], ")", " rot=(", placement.rotation[0], ", ", placement.rotation[1], ", ", placement.rotation[2], ")",
@ -286,7 +286,7 @@ void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain)
} }
} }
LOG_INFO("Loaded ", terrain.wmoPlacements.size(), " WMO placements"); LOG_DEBUG("Loaded ", terrain.wmoPlacements.size(), " WMO placements");
} }
void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTTerrain& terrain) { void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTTerrain& terrain) {
@ -321,7 +321,7 @@ void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTT
// Debug first chunk only // Debug first chunk only
if (chunkIndex == 0) { if (chunkIndex == 0) {
LOG_INFO("MCNK[0] offsets: nLayers=", nLayers, LOG_DEBUG("MCNK[0] offsets: nLayers=", nLayers,
" height=", ofsHeight, " normal=", ofsNormal, " height=", ofsHeight, " normal=", ofsNormal,
" layer=", ofsLayer, " alpha=", ofsAlpha, " layer=", ofsLayer, " alpha=", ofsAlpha,
" sizeAlpha=", sizeAlpha, " size=", size, " sizeAlpha=", sizeAlpha, " size=", size,
@ -345,7 +345,7 @@ void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTT
if (possibleMagic == MCVT) { if (possibleMagic == MCVT) {
headerSkip = 8; // Skip magic + size headerSkip = 8; // Skip magic + size
if (chunkIndex == 0) { if (chunkIndex == 0) {
LOG_INFO("MCNK sub-chunks have headers (MCVT magic found at offset ", ofsHeight, ")"); LOG_DEBUG("MCNK sub-chunks have headers (MCVT magic found at offset ", ofsHeight, ")");
} }
} }
parseMCVT(data + ofsHeight + headerSkip, 580, chunk); parseMCVT(data + ofsHeight + headerSkip, 580, chunk);
@ -434,7 +434,7 @@ void ADTLoader::parseMCLY(const uint8_t* data, size_t size, MapChunk& chunk) {
layer.effectId = readUInt32(data, i * 16 + 12); layer.effectId = readUInt32(data, i * 16 + 12);
if (layerLogCount < 10) { if (layerLogCount < 10) {
LOG_INFO(" MCLY[", i, "]: texId=", layer.textureId, LOG_DEBUG(" MCLY[", i, "]: texId=", layer.textureId,
" flags=0x", std::hex, layer.flags, std::dec, " flags=0x", std::hex, layer.flags, std::dec,
" alphaOfs=", layer.offsetMCAL, " alphaOfs=", layer.offsetMCAL,
" useAlpha=", layer.useAlpha(), " useAlpha=", layer.useAlpha(),
@ -574,7 +574,7 @@ void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain)
} }
} }
LOG_INFO("Loaded MH2O water data: ", totalLayers, " liquid layers across ", size, " bytes"); LOG_DEBUG("Loaded MH2O water data: ", totalLayers, " liquid layers across ", size, " bytes");
} }
} // namespace pipeline } // namespace pipeline

View file

@ -83,7 +83,7 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
return BLPImage(); return BLPImage();
} }
LOG_INFO("Loaded texture: ", normalizedPath, " (", image.width, "x", image.height, ")"); LOG_DEBUG("Loaded texture: ", normalizedPath, " (", image.width, "x", image.height, ")");
return image; return image;
} }

View file

@ -967,7 +967,7 @@ void M2Loader::loadAnimFile(const std::vector<uint8_t>& m2Data,
patchTrack(db.scale, bone.scale, TrackType::VEC3); patchTrack(db.scale, bone.scale, TrackType::VEC3);
} }
core::Logger::getInstance().info("Loaded .anim for sequence ", sequenceIndex, core::Logger::getInstance().debug("Loaded .anim for sequence ", sequenceIndex,
" (id=", model.sequences[sequenceIndex].id, "): patched ", patchedTracks, " bone tracks"); " (id=", model.sequences[sequenceIndex].id, "): patched ", patchedTracks, " bone tracks");
} }

View file

@ -4,6 +4,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cctype>
#ifdef HAVE_STORMLIB #ifdef HAVE_STORMLIB
#include <StormLib.h> #include <StormLib.h>
@ -21,6 +22,14 @@ typedef void* HANDLE;
namespace wowee { namespace wowee {
namespace pipeline { namespace pipeline {
namespace {
std::string toLowerCopy(std::string value) {
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return value;
}
}
MPQManager::MPQManager() = default; MPQManager::MPQManager() = default;
MPQManager::~MPQManager() { MPQManager::~MPQManager() {
@ -95,6 +104,10 @@ void MPQManager::shutdown() {
archives.clear(); archives.clear();
archiveNames.clear(); archiveNames.clear();
{
std::lock_guard<std::mutex> lock(missingFileMutex_);
missingFileWarnings_.clear();
}
initialized = false; initialized = false;
} }
@ -229,7 +242,7 @@ std::vector<uint8_t> MPQManager::readFile(const std::string& filename) const {
} }
} }
if (!found) { if (!found) {
LOG_WARNING("File not found: ", filename); logMissingFileOnce(filename);
return std::vector<uint8_t>(); return std::vector<uint8_t>();
} }
} }
@ -247,10 +260,18 @@ std::vector<uint8_t> MPQManager::readFile(const std::string& filename) const {
} }
} }
LOG_WARNING("File not found: ", filename); logMissingFileOnce(filename);
return std::vector<uint8_t>(); return std::vector<uint8_t>();
} }
void MPQManager::logMissingFileOnce(const std::string& filename) const {
std::string normalized = toLowerCopy(filename);
std::lock_guard<std::mutex> lock(missingFileMutex_);
if (missingFileWarnings_.insert(normalized).second) {
LOG_WARNING("File not found: ", filename);
}
}
uint32_t MPQManager::getFileSize(const std::string& filename) const { uint32_t MPQManager::getFileSize(const std::string& filename) const {
#ifndef HAVE_STORMLIB #ifndef HAVE_STORMLIB
return 0; return 0;

View file

@ -123,7 +123,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
// flags and numLod (uint16 each) - skip for now // flags and numLod (uint16 each) - skip for now
offset += 4; offset += 4;
core::Logger::getInstance().info("WMO header: nTextures=", model.nTextures, " nGroups=", model.nGroups); core::Logger::getInstance().debug("WMO header: nTextures=", model.nTextures, " nGroups=", model.nGroups);
break; break;
} }
@ -133,7 +133,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
// We must map every offset to its texture index. // We must map every offset to its texture index.
uint32_t texOffset = chunkStart; uint32_t texOffset = chunkStart;
uint32_t texIndex = 0; uint32_t texIndex = 0;
core::Logger::getInstance().info("MOTX chunk: ", chunkSize, " bytes"); core::Logger::getInstance().debug("MOTX chunk: ", chunkSize, " bytes");
while (texOffset < chunkEnd) { while (texOffset < chunkEnd) {
uint32_t relativeOffset = texOffset - chunkStart; uint32_t relativeOffset = texOffset - chunkStart;
@ -187,7 +187,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
model.materials.push_back(mat); model.materials.push_back(mat);
} }
core::Logger::getInstance().info("WMO materials: ", model.materials.size()); core::Logger::getInstance().debug("WMO materials: ", model.materials.size());
break; break;
} }
@ -220,7 +220,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
model.groupInfo.push_back(info); model.groupInfo.push_back(info);
} }
core::Logger::getInstance().info("WMO group info: ", model.groupInfo.size()); core::Logger::getInstance().debug("WMO group info: ", model.groupInfo.size());
break; break;
} }
@ -254,7 +254,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
model.lights.push_back(light); model.lights.push_back(light);
} }
core::Logger::getInstance().info("WMO lights: ", model.lights.size()); core::Logger::getInstance().debug("WMO lights: ", model.lights.size());
break; break;
} }
@ -303,7 +303,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
model.doodads.push_back(doodad); model.doodads.push_back(doodad);
} }
core::Logger::getInstance().info("WMO doodads: ", model.doodads.size()); core::Logger::getInstance().debug("WMO doodads: ", model.doodads.size());
break; break;
} }
@ -320,7 +320,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
model.doodadSets.push_back(set); model.doodadSets.push_back(set);
} }
core::Logger::getInstance().info("WMO doodad sets: ", model.doodadSets.size()); core::Logger::getInstance().debug("WMO doodad sets: ", model.doodadSets.size());
break; break;
} }
@ -352,7 +352,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
model.portals.push_back(portal); model.portals.push_back(portal);
} }
core::Logger::getInstance().info("WMO portals: ", model.portals.size()); core::Logger::getInstance().debug("WMO portals: ", model.portals.size());
break; break;
} }
@ -367,7 +367,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
ref.padding = read<uint16_t>(wmoData, offset); ref.padding = read<uint16_t>(wmoData, offset);
model.portalRefs.push_back(ref); model.portalRefs.push_back(ref);
} }
core::Logger::getInstance().info("WMO portal refs: ", model.portalRefs.size()); core::Logger::getInstance().debug("WMO portal refs: ", model.portalRefs.size());
break; break;
} }
@ -429,8 +429,8 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
uint32_t mogpOffset = offset; uint32_t mogpOffset = offset;
group.flags = read<uint32_t>(groupData, mogpOffset); group.flags = read<uint32_t>(groupData, mogpOffset);
bool isInterior = (group.flags & 0x2000) != 0; bool isInterior = (group.flags & 0x2000) != 0;
core::Logger::getInstance().info(" Group flags: 0x", std::hex, group.flags, std::dec, core::Logger::getInstance().debug(" Group flags: 0x", std::hex, group.flags, std::dec,
(isInterior ? " (INTERIOR)" : " (exterior)")); (isInterior ? " (INTERIOR)" : " (exterior)"));
group.boundingBoxMin.x = read<float>(groupData, mogpOffset); group.boundingBoxMin.x = read<float>(groupData, mogpOffset);
group.boundingBoxMin.y = read<float>(groupData, mogpOffset); group.boundingBoxMin.y = read<float>(groupData, mogpOffset);
group.boundingBoxMin.z = read<float>(groupData, mogpOffset); group.boundingBoxMin.z = read<float>(groupData, mogpOffset);
@ -493,7 +493,7 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
} }
else if (subChunkId == 0x4D4F4E52) { // MONR - Normals else if (subChunkId == 0x4D4F4E52) { // MONR - Normals
uint32_t normalCount = subChunkSize / 12; uint32_t normalCount = subChunkSize / 12;
core::Logger::getInstance().info(" MONR: ", normalCount, " normals for ", group.vertices.size(), " vertices"); core::Logger::getInstance().debug(" MONR: ", normalCount, " normals for ", group.vertices.size(), " vertices");
for (uint32_t i = 0; i < normalCount && i < group.vertices.size(); i++) { for (uint32_t i = 0; i < normalCount && i < group.vertices.size(); i++) {
group.vertices[i].normal.x = read<float>(groupData, mogpOffset); group.vertices[i].normal.x = read<float>(groupData, mogpOffset);
group.vertices[i].normal.y = read<float>(groupData, mogpOffset); group.vertices[i].normal.y = read<float>(groupData, mogpOffset);
@ -507,7 +507,7 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
else if (subChunkId == 0x4D4F5456) { // MOTV - Texture coords else if (subChunkId == 0x4D4F5456) { // MOTV - Texture coords
// Update texture coords for existing vertices // Update texture coords for existing vertices
uint32_t texCoordCount = subChunkSize / 8; uint32_t texCoordCount = subChunkSize / 8;
core::Logger::getInstance().info(" MOTV: ", texCoordCount, " tex coords for ", group.vertices.size(), " vertices"); core::Logger::getInstance().debug(" MOTV: ", texCoordCount, " tex coords for ", group.vertices.size(), " vertices");
for (uint32_t i = 0; i < texCoordCount && i < group.vertices.size(); i++) { for (uint32_t i = 0; i < texCoordCount && i < group.vertices.size(); i++) {
group.vertices[i].texCoord.x = read<float>(groupData, mogpOffset); group.vertices[i].texCoord.x = read<float>(groupData, mogpOffset);
group.vertices[i].texCoord.y = read<float>(groupData, mogpOffset); group.vertices[i].texCoord.y = read<float>(groupData, mogpOffset);
@ -519,7 +519,7 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
else if (subChunkId == 0x4D4F4356) { // MOCV - Vertex colors else if (subChunkId == 0x4D4F4356) { // MOCV - Vertex colors
// Update vertex colors // Update vertex colors
uint32_t colorCount = subChunkSize / 4; uint32_t colorCount = subChunkSize / 4;
core::Logger::getInstance().info(" MOCV: ", colorCount, " vertex colors for ", group.vertices.size(), " vertices"); core::Logger::getInstance().debug(" MOCV: ", colorCount, " vertex colors for ", group.vertices.size(), " vertices");
for (uint32_t i = 0; i < colorCount && i < group.vertices.size(); i++) { for (uint32_t i = 0; i < colorCount && i < group.vertices.size(); i++) {
uint8_t b = read<uint8_t>(groupData, mogpOffset); uint8_t b = read<uint8_t>(groupData, mogpOffset);
uint8_t g = read<uint8_t>(groupData, mogpOffset); uint8_t g = read<uint8_t>(groupData, mogpOffset);
@ -555,7 +555,7 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
static int batchLogCount = 0; static int batchLogCount = 0;
if (batchLogCount < 15) { if (batchLogCount < 15) {
core::Logger::getInstance().info(" Batch[", i, "]: start=", batch.startIndex, core::Logger::getInstance().debug(" Batch[", i, "]: start=", batch.startIndex,
" count=", batch.indexCount, " verts=[", batch.startVertex, "-", " count=", batch.indexCount, " verts=[", batch.startVertex, "-",
batch.lastVertex, "] mat=", (int)batch.materialId, " flags=", (int)batch.flags); batch.lastVertex, "] mat=", (int)batch.materialId, " flags=", (int)batch.flags);
batchLogCount++; batchLogCount++;
@ -633,10 +633,10 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
group.batches.push_back(batch); group.batches.push_back(batch);
} }
core::Logger::getInstance().info("WMO group ", groupIndex, " loaded: ", core::Logger::getInstance().debug("WMO group ", groupIndex, " loaded: ",
group.vertices.size(), " vertices, ", group.vertices.size(), " vertices, ",
group.indices.size(), " indices, ", group.indices.size(), " indices, ",
group.batches.size(), " batches"); group.batches.size(), " batches");
return !group.vertices.empty() && !group.indices.empty(); return !group.vertices.empty() && !group.indices.empty();
} }

View file

@ -898,7 +898,8 @@ void CameraController::update(float deltaTime) {
// WoW fades between ~1.0m and ~0.5m, hides fully below 0.5m // WoW fades between ~1.0m and ~0.5m, hides fully below 0.5m
// For now, just hide below first-person threshold // For now, just hide below first-person threshold
if (characterRenderer && playerInstanceId > 0) { if (characterRenderer && playerInstanceId > 0) {
bool shouldHidePlayer = (actualDist < MIN_DISTANCE + 0.1f); // Hide in first-person // Honor first-person intent even if anti-clipping pushes camera back slightly.
bool shouldHidePlayer = isFirstPersonView() || (actualDist < MIN_DISTANCE + 0.1f);
characterRenderer->setInstanceVisible(playerInstanceId, !shouldHidePlayer); characterRenderer->setInstanceVisible(playerInstanceId, !shouldHidePlayer);
} }
} else { } else {

View file

@ -733,7 +733,7 @@ void CharacterRenderer::setModelTexture(uint32_t modelId, uint32_t textureSlot,
} }
gpuModel.textureIds[textureSlot] = textureId; gpuModel.textureIds[textureSlot] = textureId;
core::Logger::getInstance().info("Replaced model ", modelId, " texture slot ", textureSlot, " with composited texture"); core::Logger::getInstance().debug("Replaced model ", modelId, " texture slot ", textureSlot, " with composited texture");
} }
void CharacterRenderer::resetModelTexture(uint32_t modelId, uint32_t textureSlot) { void CharacterRenderer::resetModelTexture(uint32_t modelId, uint32_t textureSlot) {
@ -773,9 +773,9 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) {
models[id] = std::move(gpuModel); models[id] = std::move(gpuModel);
core::Logger::getInstance().info("Loaded M2 model ", id, " (", model.vertices.size(), core::Logger::getInstance().debug("Loaded M2 model ", id, " (", model.vertices.size(),
" verts, ", model.bones.size(), " bones, ", model.sequences.size(), " verts, ", model.bones.size(), " bones, ", model.sequences.size(),
" anims, ", model.textures.size(), " textures)"); " anims, ", model.textures.size(), " textures)");
return true; return true;
} }
@ -1306,16 +1306,16 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
} }
if (filtered) skipped++; else rendered++; if (filtered) skipped++; else rendered++;
LOG_INFO("Batch ", bIdx, ": submesh=", b.submeshId, LOG_DEBUG("Batch ", bIdx, ": submesh=", b.submeshId,
" level=", b.submeshLevel, " level=", b.submeshLevel,
" idxStart=", b.indexStart, " idxCount=", b.indexCount, " idxStart=", b.indexStart, " idxCount=", b.indexCount,
" tex=", texInfo, " tex=", texInfo,
filtered ? " [SKIP]" : " [RENDER]"); filtered ? " [SKIP]" : " [RENDER]");
bIdx++; bIdx++;
} }
LOG_INFO("Batch summary: ", rendered, " rendered, ", skipped, " skipped, ", LOG_DEBUG("Batch summary: ", rendered, " rendered, ", skipped, " skipped, ",
gpuModel.textureIds.size(), " textures loaded, ", gpuModel.textureIds.size(), " textures loaded, ",
gpuModel.data.textureLookup.size(), " in lookup table"); gpuModel.data.textureLookup.size(), " in lookup table");
for (size_t t = 0; t < gpuModel.data.textures.size(); t++) { for (size_t t = 0; t < gpuModel.data.textures.size(); t++) {
} }
} }

View file

@ -2520,6 +2520,24 @@ void M2Renderer::removeInstance(uint32_t instanceId) {
} }
} }
void M2Renderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
if (instanceIds.empty() || instances.empty()) {
return;
}
std::unordered_set<uint32_t> toRemove(instanceIds.begin(), instanceIds.end());
const size_t oldSize = instances.size();
instances.erase(std::remove_if(instances.begin(), instances.end(),
[&toRemove](const M2Instance& inst) {
return toRemove.find(inst.id) != toRemove.end();
}),
instances.end());
if (instances.size() != oldSize) {
rebuildSpatialIndex();
}
}
void M2Renderer::clear() { void M2Renderer::clear() {
for (auto& [id, model] : models) { for (auto& [id, model] : models) {
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao); if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);

View file

@ -574,7 +574,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
} }
// Discover mount animation capabilities (property-based, not hardcoded IDs) // Discover mount animation capabilities (property-based, not hardcoded IDs)
LOG_INFO("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ==="); LOG_DEBUG("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ===");
characterRenderer->dumpAnimations(mountInstId); characterRenderer->dumpAnimations(mountInstId);
// Get all sequences for property-based analysis // Get all sequences for property-based analysis
@ -597,9 +597,9 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
// Property-based jump animation discovery with chain-based scoring // Property-based jump animation discovery with chain-based scoring
auto discoverJumpSet = [&]() { auto discoverJumpSet = [&]() {
// Debug: log all sequences for analysis // Debug: log all sequences for analysis
LOG_INFO("=== Full sequence table for mount ==="); LOG_DEBUG("=== Full sequence table for mount ===");
for (const auto& seq : sequences) { for (const auto& seq : sequences) {
LOG_INFO("SEQ id=", seq.id, LOG_DEBUG("SEQ id=", seq.id,
" dur=", seq.duration, " dur=", seq.duration,
" flags=0x", std::hex, seq.flags, std::dec, " flags=0x", std::hex, seq.flags, std::dec,
" moveSpd=", seq.movingSpeed, " moveSpd=", seq.movingSpeed,
@ -607,7 +607,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
" next=", seq.nextAnimation, " next=", seq.nextAnimation,
" alias=", seq.aliasNext); " alias=", seq.aliasNext);
} }
LOG_INFO("=== End sequence table ==="); LOG_DEBUG("=== End sequence table ===");
// Known combat/bad animation IDs to avoid // Known combat/bad animation IDs to avoid
std::set<uint32_t> forbiddenIds = {53, 54, 16}; // jumpkick, attack std::set<uint32_t> forbiddenIds = {53, 54, 16}; // jumpkick, attack
@ -701,7 +701,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
} }
} }
LOG_INFO("Property-based jump discovery: start=", start, " loop=", loop, " end=", end, LOG_DEBUG("Property-based jump discovery: start=", start, " loop=", loop, " end=", end,
" scores: start=", bestStart, " end=", bestEnd); " scores: start=", bestStart, " end=", bestEnd);
return std::make_tuple(start, loop, end); return std::make_tuple(start, loop, end);
}; };
@ -718,17 +718,17 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
// Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers) // Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers)
mountAnims_.fidgets.clear(); mountAnims_.fidgets.clear();
core::Logger::getInstance().info("Scanning for fidget animations in ", sequences.size(), " sequences"); core::Logger::getInstance().debug("Scanning for fidget animations in ", sequences.size(), " sequences");
// DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses // DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses
core::Logger::getInstance().info("=== ALL potential fidgets (no metadata filter) ==="); core::Logger::getInstance().debug("=== ALL potential fidgets (no metadata filter) ===");
for (const auto& seq : sequences) { for (const auto& seq : sequences) {
bool isLoop = (seq.flags & 0x01) == 0; bool isLoop = (seq.flags & 0x01) == 0;
bool isStationary = std::abs(seq.movingSpeed) < 0.05f; bool isStationary = std::abs(seq.movingSpeed) < 0.05f;
bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500;
if (!isLoop && reasonableDuration && isStationary) { if (!isLoop && reasonableDuration && isStationary) {
core::Logger::getInstance().info(" ALL: id=", seq.id, core::Logger::getInstance().debug(" ALL: id=", seq.id,
" dur=", seq.duration, "ms", " dur=", seq.duration, "ms",
" freq=", seq.frequency, " freq=", seq.frequency,
" replay=", seq.replayMin, "-", seq.replayMax, " replay=", seq.replayMin, "-", seq.replayMax,
@ -747,7 +747,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
// Log candidates with metadata // Log candidates with metadata
if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) { if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) {
core::Logger::getInstance().info(" Candidate: id=", seq.id, core::Logger::getInstance().debug(" Candidate: id=", seq.id,
" dur=", seq.duration, "ms", " dur=", seq.duration, "ms",
" freq=", seq.frequency, " freq=", seq.frequency,
" replay=", seq.replayMin, "-", seq.replayMax, " replay=", seq.replayMin, "-", seq.replayMax,
@ -770,7 +770,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
(seq.nextAnimation == -1); (seq.nextAnimation == -1);
mountAnims_.fidgets.push_back(seq.id); mountAnims_.fidgets.push_back(seq.id);
core::Logger::getInstance().info(" >> Selected fidget: id=", seq.id, core::Logger::getInstance().debug(" >> Selected fidget: id=", seq.id,
(chainsToStand ? " (chains to stand)" : "")); (chainsToStand ? " (chains to stand)" : ""));
} }
} }
@ -779,7 +779,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
if (mountAnims_.stand == 0) mountAnims_.stand = 0; // Force 0 even if not found if (mountAnims_.stand == 0) mountAnims_.stand = 0; // Force 0 even if not found
if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run
core::Logger::getInstance().info("Mount animation set: jumpStart=", mountAnims_.jumpStart, core::Logger::getInstance().debug("Mount animation set: jumpStart=", mountAnims_.jumpStart,
" jumpLoop=", mountAnims_.jumpLoop, " jumpLoop=", mountAnims_.jumpLoop,
" jumpEnd=", mountAnims_.jumpEnd, " jumpEnd=", mountAnims_.jumpEnd,
" rearUp=", mountAnims_.rearUp, " rearUp=", mountAnims_.rearUp,
@ -1001,7 +1001,7 @@ void Renderer::updateCharacterAnimation() {
if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) { if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) {
if (moving && mountAnims_.jumpLoop > 0) { if (moving && mountAnims_.jumpLoop > 0) {
// Moving: skip JumpStart (looks like stopping), go straight to airborne loop // Moving: skip JumpStart (looks like stopping), go straight to airborne loop
LOG_INFO("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop); LOG_DEBUG("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop);
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
mountAction_ = MountAction::Jump; mountAction_ = MountAction::Jump;
mountActionPhase_ = 1; // Start in airborne phase mountActionPhase_ = 1; // Start in airborne phase
@ -1014,7 +1014,7 @@ void Renderer::updateCharacterAnimation() {
} }
} else if (!moving && mountAnims_.rearUp > 0) { } else if (!moving && mountAnims_.rearUp > 0) {
// Standing still: rear-up flourish // Standing still: rear-up flourish
LOG_INFO("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp); LOG_DEBUG("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp);
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false); characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false);
mountAction_ = MountAction::RearUp; mountAction_ = MountAction::RearUp;
mountActionPhase_ = 0; mountActionPhase_ = 0;
@ -1035,17 +1035,17 @@ void Renderer::updateCharacterAnimation() {
// Jump sequence: start → loop → end (physics-driven) // Jump sequence: start → loop → end (physics-driven)
if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) { if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) {
// JumpStart finished, go to JumpLoop (airborne) // JumpStart finished, go to JumpLoop (airborne)
LOG_INFO("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")"); LOG_DEBUG("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")");
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
mountActionPhase_ = 1; mountActionPhase_ = 1;
mountAnimId = mountAnims_.jumpLoop; mountAnimId = mountAnims_.jumpLoop;
} else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) { } else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) {
// No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose) // No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose)
LOG_INFO("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)"); LOG_DEBUG("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)");
mountActionPhase_ = 1; mountActionPhase_ = 1;
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) { } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) {
// Landed after airborne phase! Go to JumpEnd (grounded-triggered) // Landed after airborne phase! Go to JumpEnd (grounded-triggered)
LOG_INFO("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")"); LOG_DEBUG("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")");
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false); characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false);
mountActionPhase_ = 2; mountActionPhase_ = 2;
mountAnimId = mountAnims_.jumpEnd; mountAnimId = mountAnims_.jumpEnd;
@ -1055,14 +1055,14 @@ void Renderer::updateCharacterAnimation() {
} }
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) {
// No JumpEnd animation, return directly to movement after landing // No JumpEnd animation, return directly to movement after landing
LOG_INFO("Mount jump: phase 1→done (landed, no JumpEnd, returning to ", LOG_DEBUG("Mount jump: phase 1→done (landed, no JumpEnd, returning to ",
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
mountAction_ = MountAction::None; mountAction_ = MountAction::None;
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
} else if (mountActionPhase_ == 2 && animFinished) { } else if (mountActionPhase_ == 2 && animFinished) {
// JumpEnd finished, return to movement // JumpEnd finished, return to movement
LOG_INFO("Mount jump: phase 2→done (JumpEnd finished, returning to ", LOG_DEBUG("Mount jump: phase 2→done (JumpEnd finished, returning to ",
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
mountAction_ = MountAction::None; mountAction_ = MountAction::None;
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
@ -1073,7 +1073,7 @@ void Renderer::updateCharacterAnimation() {
} else if (mountAction_ == MountAction::RearUp) { } else if (mountAction_ == MountAction::RearUp) {
// Rear-up: single animation, return to stand when done // Rear-up: single animation, return to stand when done
if (animFinished) { if (animFinished) {
LOG_INFO("Mount rear-up: finished, returning to ", LOG_DEBUG("Mount rear-up: finished, returning to ",
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand)); moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand));
mountAction_ = MountAction::None; mountAction_ = MountAction::None;
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
@ -1110,7 +1110,7 @@ void Renderer::updateCharacterAnimation() {
// If animation changed or completed, clear active fidget // If animation changed or completed, clear active fidget
if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) { if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) {
mountActiveFidget_ = 0; mountActiveFidget_ = 0;
LOG_INFO("Mount fidget completed"); LOG_DEBUG("Mount fidget completed");
} }
} }
} }
@ -1131,7 +1131,7 @@ void Renderer::updateCharacterAnimation() {
mountIdleFidgetTimer_ = 0.0f; mountIdleFidgetTimer_ = 0.0f;
nextFidgetTime = 6.0f + (rand() % 7); // Randomize next fidget time nextFidgetTime = 6.0f + (rand() % 7); // Randomize next fidget time
LOG_INFO("Mount idle fidget: playing anim ", fidgetAnim); LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim);
} }
} }
if (moving) { if (moving) {
@ -1666,6 +1666,10 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const {
} }
void Renderer::update(float deltaTime) { void Renderer::update(float deltaTime) {
if (musicSwitchCooldown_ > 0.0f) {
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime);
}
auto updateStart = std::chrono::steady_clock::now(); auto updateStart = std::chrono::steady_clock::now();
lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation() lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation()
@ -1696,6 +1700,14 @@ void Renderer::update(float deltaTime) {
auto cam2 = std::chrono::high_resolution_clock::now(); auto cam2 = std::chrono::high_resolution_clock::now();
camTime += std::chrono::duration<float, std::milli>(cam2 - cam1).count(); camTime += std::chrono::duration<float, std::milli>(cam2 - cam1).count();
// Visibility hardening: ensure player instance cannot stay hidden after
// taxi/camera transitions, but preserve first-person self-hide.
if (characterRenderer && characterInstanceId > 0 && cameraController) {
if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || taxiFlight_) {
characterRenderer->setInstanceVisible(characterInstanceId, true);
}
}
// Update lighting system // Update lighting system
auto light1 = std::chrono::high_resolution_clock::now(); auto light1 = std::chrono::high_resolution_clock::now();
if (lightingManager) { if (lightingManager) {
@ -2082,6 +2094,7 @@ void Renderer::update(float deltaTime) {
inTavern_ = true; inTavern_ = true;
LOG_INFO("Entered tavern"); LOG_INFO("Entered tavern");
musicManager->playMusic(tavernMusic, true); // Immediate playback, looping musicManager->playMusic(tavernMusic, true); // Immediate playback, looping
musicSwitchCooldown_ = 6.0f;
} }
} else if (inTavern_) { } else if (inTavern_) {
// Exited tavern - restore zone music with crossfade // Exited tavern - restore zone music with crossfade
@ -2092,6 +2105,7 @@ void Renderer::update(float deltaTime) {
std::string music = zoneManager->getRandomMusic(currentZoneId); std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) { if (!music.empty()) {
musicManager->crossfadeTo(music); musicManager->crossfadeTo(music);
musicSwitchCooldown_ = 6.0f;
} }
} }
} }
@ -2112,6 +2126,7 @@ void Renderer::update(float deltaTime) {
std::string music = zoneManager->getRandomMusic(currentZoneId); std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) { if (!music.empty()) {
musicManager->crossfadeTo(music); musicManager->crossfadeTo(music);
musicSwitchCooldown_ = 6.0f;
} }
} }
} }
@ -2123,9 +2138,12 @@ void Renderer::update(float deltaTime) {
if (info) { if (info) {
currentZoneName = info->name; currentZoneName = info->name;
LOG_INFO("Entered zone: ", info->name); LOG_INFO("Entered zone: ", info->name);
std::string music = zoneManager->getRandomMusic(zoneId); if (musicSwitchCooldown_ <= 0.0f) {
if (!music.empty()) { std::string music = zoneManager->getRandomMusic(zoneId);
musicManager->crossfadeTo(music); if (!music.empty()) {
musicManager->crossfadeTo(music);
musicSwitchCooldown_ = 6.0f;
}
} }
} }
} }

View file

@ -244,7 +244,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
auto adtData = assetManager->readFile(adtPath); auto adtData = assetManager->readFile(adtPath);
if (adtData.empty()) { if (adtData.empty()) {
LOG_WARNING("Failed to load ADT file: ", adtPath); logMissingAdtOnce(adtPath);
return nullptr; return nullptr;
} }
@ -322,7 +322,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
preparedModelIds.insert(modelId); preparedModelIds.insert(modelId);
} else { } else {
skippedInvalid++; skippedInvalid++;
LOG_WARNING("M2 model invalid (no verts/indices): ", m2Path); LOG_DEBUG("M2 model invalid (no verts/indices): ", m2Path);
} }
} else { } else {
skippedFileNotFound++; skippedFileNotFound++;
@ -352,7 +352,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
} }
if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) { if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) {
LOG_WARNING("Tile [", x, ",", y, "] doodad issues: ", LOG_DEBUG("Tile [", x, ",", y, "] doodad issues: ",
skippedNameId, " bad nameId, ", skippedNameId, " bad nameId, ",
skippedFileNotFound, " file not found, ", skippedFileNotFound, " file not found, ",
skippedInvalid, " invalid model, ", skippedInvalid, " invalid model, ",
@ -547,6 +547,17 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
return pending; return pending;
} }
void TerrainManager::logMissingAdtOnce(const std::string& adtPath) {
std::string normalized = adtPath;
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::lock_guard<std::mutex> lock(missingAdtWarningsMutex_);
if (missingAdtWarnings_.insert(normalized).second) {
LOG_WARNING("Failed to load ADT file: ", adtPath);
}
}
void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) { void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
int x = pending->coord.x; int x = pending->coord.x;
int y = pending->coord.y; int y = pending->coord.y;
@ -723,7 +734,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
} }
} }
if (loadedWMOs > 0 || skippedWmoDedup > 0) { if (loadedWMOs > 0 || skippedWmoDedup > 0) {
LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ",
loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped"); loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped");
} }
if (loadedLiquids > 0) { if (loadedLiquids > 0) {
@ -817,8 +828,8 @@ void TerrainManager::workerLoop() {
void TerrainManager::processReadyTiles() { void TerrainManager::processReadyTiles() {
// Process tiles with time budget to avoid frame spikes // Process tiles with time budget to avoid frame spikes
// Budget: 5ms per frame (allows 3 tiles at ~1.5ms each or 1 heavy tile) // Taxi mode gets a slightly larger budget to avoid visible late-pop terrain/models.
const float timeBudgetMs = 5.0f; const float timeBudgetMs = taxiStreamingMode_ ? 8.0f : 5.0f;
auto startTime = std::chrono::high_resolution_clock::now(); auto startTime = std::chrono::high_resolution_clock::now();
int processed = 0; int processed = 0;
@ -1010,9 +1021,7 @@ void TerrainManager::unloadTile(int x, int y) {
// Remove M2 doodad instances // Remove M2 doodad instances
if (m2Renderer) { if (m2Renderer) {
for (uint32_t id : tile->m2InstanceIds) { m2Renderer->removeInstances(tile->m2InstanceIds);
m2Renderer->removeInstance(id);
}
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances"); LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
} }
@ -1023,8 +1032,8 @@ void TerrainManager::unloadTile(int x, int y) {
if (waterRenderer) { if (waterRenderer) {
waterRenderer->removeWMO(id); waterRenderer->removeWMO(id);
} }
wmoRenderer->removeInstance(id);
} }
wmoRenderer->removeInstances(tile->wmoInstanceIds);
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances"); LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");
} }
@ -1328,6 +1337,18 @@ std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float
} }
void TerrainManager::streamTiles() { void TerrainManager::streamTiles() {
auto shouldSkipMissingAdt = [this](const TileCoord& coord) -> bool {
if (!assetManager) return false;
if (failedTiles.find(coord) != failedTiles.end()) return true;
const std::string adtPath = getADTPath(coord);
if (!assetManager->fileExists(adtPath)) {
// Mark permanently failed so future stream/precache passes do not retry.
failedTiles[coord] = true;
return true;
}
return false;
};
// Enqueue tiles in radius around current tile for async loading // Enqueue tiles in radius around current tile for async loading
{ {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
@ -1353,6 +1374,7 @@ void TerrainManager::streamTiles() {
if (loadedTiles.find(coord) != loadedTiles.end()) continue; if (loadedTiles.find(coord) != loadedTiles.end()) continue;
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;
if (shouldSkipMissingAdt(coord)) continue;
loadQueue.push_back(coord); loadQueue.push_back(coord);
pendingTiles[coord] = true; pendingTiles[coord] = true;
@ -1403,12 +1425,18 @@ void TerrainManager::precacheTiles(const std::vector<std::pair<int, int>>& tiles
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
for (const auto& [x, y] : tiles) { for (const auto& [x, y] : tiles) {
if (x < 0 || x > 63 || y < 0 || y > 63) continue;
TileCoord coord = {x, y}; TileCoord coord = {x, y};
// Skip if already loaded, pending, or failed // Skip if already loaded, pending, or failed
if (loadedTiles.find(coord) != loadedTiles.end()) continue; if (loadedTiles.find(coord) != loadedTiles.end()) continue;
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;
if (assetManager && !assetManager->fileExists(getADTPath(coord))) {
failedTiles[coord] = true;
continue;
}
// Precache work is prioritized so taxi-route tiles are prepared before // Precache work is prioritized so taxi-route tiles are prepared before
// opportunistic radius streaming tiles. // opportunistic radius streaming tiles.

View file

@ -46,6 +46,17 @@ bool TerrainRenderer::initialize(pipeline::AssetManager* assets) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
// Create default opaque alpha texture for terrain layer masks
uint8_t opaqueAlpha = 255;
glGenTextures(1, &opaqueAlphaTexture);
glBindTexture(GL_TEXTURE_2D, opaqueAlphaTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &opaqueAlpha);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
LOG_INFO("Terrain renderer initialized"); LOG_INFO("Terrain renderer initialized");
return true; return true;
} }
@ -60,6 +71,10 @@ void TerrainRenderer::shutdown() {
glDeleteTextures(1, &whiteTexture); glDeleteTextures(1, &whiteTexture);
whiteTexture = 0; whiteTexture = 0;
} }
if (opaqueAlphaTexture) {
glDeleteTextures(1, &opaqueAlphaTexture);
opaqueAlphaTexture = 0;
}
// Delete cached textures // Delete cached textures
for (auto& pair : textureCache) { for (auto& pair : textureCache) {
@ -73,7 +88,7 @@ void TerrainRenderer::shutdown() {
bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh, bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
const std::vector<std::string>& texturePaths, const std::vector<std::string>& texturePaths,
int tileX, int tileY) { int tileX, int tileY) {
LOG_INFO("Loading terrain mesh: ", mesh.validChunkCount, " chunks"); LOG_DEBUG("Loading terrain mesh: ", mesh.validChunkCount, " chunks");
// Upload each chunk to GPU // Upload each chunk to GPU
for (int y = 0; y < 16; y++) { for (int y = 0; y < 16; y++) {
@ -116,7 +131,7 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
gpuChunk.layerTextures.push_back(layerTex); gpuChunk.layerTextures.push_back(layerTex);
// Create alpha texture // Create alpha texture
GLuint alphaTex = 0; GLuint alphaTex = opaqueAlphaTexture;
if (!layer.alphaData.empty()) { if (!layer.alphaData.empty()) {
alphaTex = createAlphaTexture(layer.alphaData); alphaTex = createAlphaTexture(layer.alphaData);
} }
@ -133,7 +148,7 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
} }
} }
LOG_INFO("Loaded ", chunks.size(), " terrain chunks to GPU"); LOG_DEBUG("Loaded ", chunks.size(), " terrain chunks to GPU");
return !chunks.empty(); return !chunks.empty();
} }
@ -218,7 +233,8 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) {
pipeline::BLPImage blp = assetManager->loadTexture(path); pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) { if (!blp.isValid()) {
LOG_WARNING("Failed to load texture: ", path); LOG_WARNING("Failed to load texture: ", path);
textureCache[path] = whiteTexture; // Do not cache failure as white: MPQ/file reads can fail transiently
// during heavy streaming and should be allowed to recover.
return whiteTexture; return whiteTexture;
} }
@ -257,7 +273,8 @@ void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map<std::stri
// Skip if already cached // Skip if already cached
if (textureCache.find(path) != textureCache.end()) continue; if (textureCache.find(path) != textureCache.end()) continue;
if (!blp.isValid()) { if (!blp.isValid()) {
textureCache[path] = whiteTexture; // Don't poison cache with white on invalid preload; allow fallback
// path to retry loading this texture later.
continue; continue;
} }
@ -281,20 +298,32 @@ void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map<std::stri
GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData) { GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData) {
if (alphaData.empty()) { if (alphaData.empty()) {
return 0; return opaqueAlphaTexture;
}
if (alphaData.size() != 4096) {
LOG_WARNING("Unexpected terrain alpha size: ", alphaData.size(), " (expected 4096)");
} }
GLuint textureID; GLuint textureID;
glGenTextures(1, &textureID); glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID); glBindTexture(GL_TEXTURE_2D, textureID);
// Alpha data is always expanded to 4096 bytes (64x64 at 8-bit) by terrain_mesh // Alpha data should be 64x64 (4096 bytes). Clamp to a sane fallback when malformed.
std::vector<uint8_t> expanded;
const uint8_t* src = alphaData.data();
if (alphaData.size() < 4096) {
expanded.assign(4096, 255);
std::copy(alphaData.begin(), alphaData.end(), expanded.begin());
src = expanded.data();
}
int width = 64; int width = 64;
int height = static_cast<int>(alphaData.size()) / 64; int height = 64;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, glTexImage2D(GL_TEXTURE_2D, 0, GL_RED,
width, height, 0, width, height, 0,
GL_RED, GL_UNSIGNED_BYTE, alphaData.data()); GL_RED, GL_UNSIGNED_BYTE, src);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
@ -329,6 +358,9 @@ void TerrainRenderer::render(const Camera& camera) {
// Enable depth testing // Enable depth testing
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS); glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDisable(GL_BLEND);
// Disable backface culling temporarily to debug flashing // Disable backface culling temporarily to debug flashing
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);

View file

@ -184,10 +184,10 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
constexpr float TILE_SIZE = 33.33333f / 8.0f; constexpr float TILE_SIZE = 33.33333f / 8.0f;
if (!append) { if (!append) {
LOG_INFO("Loading water from terrain (replacing)"); LOG_DEBUG("Loading water from terrain (replacing)");
clear(); clear();
} else { } else {
LOG_INFO("Loading water from terrain (appending)"); LOG_DEBUG("Loading water from terrain (appending)");
} }
// Load water surfaces from MH2O data // Load water surfaces from MH2O data
@ -285,14 +285,14 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
glm::vec2(moonwellPos.x, moonwellPos.y)); glm::vec2(moonwellPos.x, moonwellPos.y));
if (distToMoonwell > 300.0f) { // Terrain tiles are large, use bigger exclusion radius if (distToMoonwell > 300.0f) { // Terrain tiles are large, use bigger exclusion radius
LOG_INFO(" -> LOWERING water at tile (", tileX, ",", tileY, ") from height ", layer.minHeight, " by 1 unit"); LOG_DEBUG(" -> LOWERING water at tile (", tileX, ",", tileY, ") from height ", layer.minHeight, " by 1 unit");
for (float& h : surface.heights) { for (float& h : surface.heights) {
h -= 1.0f; h -= 1.0f;
} }
surface.minHeight -= 1.0f; surface.minHeight -= 1.0f;
surface.maxHeight -= 1.0f; surface.maxHeight -= 1.0f;
} else { } else {
LOG_INFO(" -> SKIPPING tile (", tileX, ",", tileY, ") - moonwell exclusion (dist: ", distToMoonwell, ")"); LOG_DEBUG(" -> SKIPPING tile (", tileX, ",", tileY, ") - moonwell exclusion (dist: ", distToMoonwell, ")");
} }
} }
@ -307,7 +307,7 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
} }
} }
LOG_INFO("Loaded ", totalLayers, " water layers from MH2O data"); LOG_DEBUG("Loaded ", totalLayers, " water layers from MH2O data");
} }
void WaterRenderer::removeTile(int tileX, int tileY) { void WaterRenderer::removeTile(int tileX, int tileY) {
@ -391,7 +391,7 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
int tileY = static_cast<int>(std::floor((32.0f - surface.origin.y / 533.33333f))); int tileY = static_cast<int>(std::floor((32.0f - surface.origin.y / 533.33333f)));
// Log all WMO water to debug park issue // Log all WMO water to debug park issue
LOG_INFO("WMO water at pos=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z, LOG_DEBUG("WMO water at pos=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
") tile=(", tileX, ",", tileY, ") wmoId=", wmoId); ") tile=(", tileX, ",", tileY, ") wmoId=", wmoId);
// Expanded bounds to cover all of Stormwind including outlying areas and park // Expanded bounds to cover all of Stormwind including outlying areas and park
@ -405,27 +405,27 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
glm::vec2(moonwellPos.x, moonwellPos.y)); glm::vec2(moonwellPos.x, moonwellPos.y));
if (distToMoonwell > 20.0f) { if (distToMoonwell > 20.0f) {
LOG_INFO(" -> LOWERING by 1 unit (dist to moonwell: ", distToMoonwell, ")"); LOG_DEBUG(" -> LOWERING by 1 unit (dist to moonwell: ", distToMoonwell, ")");
for (float& h : surface.heights) { for (float& h : surface.heights) {
h -= 1.0f; h -= 1.0f;
} }
surface.minHeight -= 1.0f; surface.minHeight -= 1.0f;
surface.maxHeight -= 1.0f; surface.maxHeight -= 1.0f;
} else { } else {
LOG_INFO(" -> SKIPPING (moonwell exclusion zone, dist: ", distToMoonwell, ")"); LOG_DEBUG(" -> SKIPPING (moonwell exclusion zone, dist: ", distToMoonwell, ")");
} }
} }
// Skip WMO water that's clearly invalid (extremely high - above 300 units) // Skip WMO water that's clearly invalid (extremely high - above 300 units)
// This is a conservative global filter that won't affect normal gameplay // This is a conservative global filter that won't affect normal gameplay
if (surface.origin.z > 300.0f) { if (surface.origin.z > 300.0f) {
LOG_INFO("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too high)"); LOG_DEBUG("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too high)");
return; return;
} }
// Skip WMO water that's extremely low (deep underground where it shouldn't be) // Skip WMO water that's extremely low (deep underground where it shouldn't be)
if (surface.origin.z < -100.0f) { if (surface.origin.z < -100.0f) {
LOG_INFO("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too low)"); LOG_DEBUG("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too low)");
return; return;
} }

View file

@ -268,8 +268,8 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
return true; return true;
} }
core::Logger::getInstance().info("Loading WMO model ", id, " with ", model.groups.size(), " groups, ", core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
model.textures.size(), " textures..."); model.textures.size(), " textures...");
ModelData modelData; ModelData modelData;
modelData.id = id; modelData.id = id;
@ -282,11 +282,11 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
modelData.isLowPlatform = (vert < 6.0f && horiz > 20.0f); modelData.isLowPlatform = (vert < 6.0f && horiz > 20.0f);
} }
core::Logger::getInstance().info(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z, core::Logger::getInstance().debug(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")"); ") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
// Load textures for this model // Load textures for this model
core::Logger::getInstance().info(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials"); core::Logger::getInstance().debug(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
if (assetManager && !model.textures.empty()) { if (assetManager && !model.textures.empty()) {
for (size_t i = 0; i < model.textures.size(); i++) { for (size_t i = 0; i < model.textures.size(); i++) {
const auto& texPath = model.textures[i]; const auto& texPath = model.textures[i];
@ -294,13 +294,13 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
GLuint texId = loadTexture(texPath); GLuint texId = loadTexture(texPath);
modelData.textures.push_back(texId); modelData.textures.push_back(texId);
} }
core::Logger::getInstance().info(" Loaded ", modelData.textures.size(), " textures for WMO"); core::Logger::getInstance().debug(" Loaded ", modelData.textures.size(), " textures for WMO");
} }
// Store material -> texture index mapping // Store material -> texture index mapping
// IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index! // IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index!
// We need to convert it using the textureOffsetToIndex map // We need to convert it using the textureOffsetToIndex map
core::Logger::getInstance().info(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries"); core::Logger::getInstance().debug(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
static int matLogCount = 0; static int matLogCount = 0;
for (size_t i = 0; i < model.materials.size(); i++) { for (size_t i = 0; i < model.materials.size(); i++) {
const auto& mat = model.materials[i]; const auto& mat = model.materials[i];
@ -310,19 +310,19 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
if (it != model.textureOffsetToIndex.end()) { if (it != model.textureOffsetToIndex.end()) {
texIndex = it->second; texIndex = it->second;
if (matLogCount < 20) { if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex); core::Logger::getInstance().debug(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex);
matLogCount++; matLogCount++;
} }
} else if (mat.texture1 < model.textures.size()) { } else if (mat.texture1 < model.textures.size()) {
// Fallback: maybe it IS an index in some files? // Fallback: maybe it IS an index in some files?
texIndex = mat.texture1; texIndex = mat.texture1;
if (matLogCount < 20) { if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": using texture1 as direct index: ", texIndex); core::Logger::getInstance().debug(" Material ", i, ": using texture1 as direct index: ", texIndex);
matLogCount++; matLogCount++;
} }
} else { } else {
if (matLogCount < 20) { if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default"); core::Logger::getInstance().debug(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default");
matLogCount++; matLogCount++;
} }
} }
@ -435,8 +435,8 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
} }
if (!modelData.portals.empty()) { if (!modelData.portals.empty()) {
core::Logger::getInstance().info("WMO portals: ", modelData.portals.size(), core::Logger::getInstance().debug("WMO portals: ", modelData.portals.size(),
" refs: ", modelData.portalRefs.size()); " refs: ", modelData.portalRefs.size());
} }
// Store doodad templates (M2 models placed in WMO) for instancing later // Store doodad templates (M2 models placed in WMO) for instancing later
@ -478,12 +478,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
} }
if (!modelData.doodadTemplates.empty()) { if (!modelData.doodadTemplates.empty()) {
core::Logger::getInstance().info("WMO has ", modelData.doodadTemplates.size(), " doodad templates"); core::Logger::getInstance().debug("WMO has ", modelData.doodadTemplates.size(), " doodad templates");
} }
} }
loadedModels[id] = std::move(modelData); loadedModels[id] = std::move(modelData);
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)"); core::Logger::getInstance().debug("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
return true; return true;
} }
@ -570,7 +570,7 @@ uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position
} }
} }
} }
core::Logger::getInstance().info("Created WMO instance ", instance.id, " (model ", modelId, ")"); core::Logger::getInstance().debug("Created WMO instance ", instance.id, " (model ", modelId, ")");
return instance.id; return instance.id;
} }
@ -677,7 +677,27 @@ void WMORenderer::removeInstance(uint32_t instanceId) {
if (it != instances.end()) { if (it != instances.end()) {
instances.erase(it); instances.erase(it);
rebuildSpatialIndex(); rebuildSpatialIndex();
core::Logger::getInstance().info("Removed WMO instance ", instanceId); core::Logger::getInstance().debug("Removed WMO instance ", instanceId);
}
}
void WMORenderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
if (instanceIds.empty() || instances.empty()) {
return;
}
std::unordered_set<uint32_t> toRemove(instanceIds.begin(), instanceIds.end());
const size_t oldSize = instances.size();
instances.erase(std::remove_if(instances.begin(), instances.end(),
[&toRemove](const WMOInstance& inst) {
return toRemove.find(inst.id) != toRemove.end();
}),
instances.end());
if (instances.size() != oldSize) {
rebuildSpatialIndex();
core::Logger::getInstance().debug("Removed ", (oldSize - instances.size()),
" WMO instances (batched)");
} }
} }
@ -1582,7 +1602,9 @@ GLuint WMORenderer::loadTexture(const std::string& path) {
pipeline::BLPImage blp = assetManager->loadTexture(path); pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) { if (!blp.isValid()) {
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path); core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
textureCache[path] = whiteTexture; // Do not cache failures as white. MPQ reads can fail transiently
// during streaming/contention, and caching white here permanently
// poisons the texture for this session.
return whiteTexture; return whiteTexture;
} }