diff --git a/include/core/application.hpp b/include/core/application.hpp index 08da7458..2f73d0ca 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -184,6 +184,8 @@ private: std::unordered_map displayIdModelCache_; // displayId → modelId (model caching) mutable std::unordered_set warnedMissingDisplayDataIds_; // displayIds already warned mutable std::unordered_set warnedMissingModelPathIds_; // modelIds/displayIds already warned + mutable std::unordered_map missingDisplayFallbackPathCache_; // missing displayId -> fallback model path + mutable std::unordered_set warnedMissingDisplayFallbackIds_; // displayIds logged for fallback usage uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures uint32_t gryphonDisplayId_ = 0; uint32_t wyvernDisplayId_ = 0; diff --git a/src/core/application.cpp b/src/core/application.cpp index ec1e39fa..b3f0023c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -496,6 +496,8 @@ void Application::reloadExpansionData() { creatureModelIds_.clear(); creatureRenderPosCache_.clear(); nonRenderableCreatureDisplayIds_.clear(); + missingDisplayFallbackPathCache_.clear(); + warnedMissingDisplayFallbackIds_.clear(); buildCreatureDisplayLookups(); } @@ -510,6 +512,8 @@ void Application::logoutToLogin() { wasAutoAttacking_ = false; loadedMapId_ = 0xFFFFFFFF; nonRenderableCreatureDisplayIds_.clear(); + missingDisplayFallbackPathCache_.clear(); + warnedMissingDisplayFallbackIds_.clear(); world.reset(); if (renderer) { // Remove old player model so it doesn't persist into next session @@ -3480,6 +3484,15 @@ std::string Application::getModelPathForDisplayId(uint32_t displayId) const { auto itData = displayDataMap_.find(displayId); if (itData == displayDataMap_.end()) { + if (displayId > 1000000u) { + static uint32_t suspiciousDisplayIdDrops = 0; + ++suspiciousDisplayIdDrops; + if (suspiciousDisplayIdDrops <= 3 || (suspiciousDisplayIdDrops % 100) == 0) { + LOG_WARNING("Skipping suspicious displayId ", displayId, + " (likely malformed movement/update parse)"); + } + return ""; + } // Some sources (e.g., taxi nodes) may provide a modelId directly. auto itPath = modelIdToPath_.find(displayId); if (itPath != modelIdToPath_.end()) { @@ -3487,6 +3500,40 @@ std::string Application::getModelPathForDisplayId(uint32_t displayId) const { } if (displayId == 30412) return "Creature\\Gryphon\\Gryphon.m2"; if (displayId == 30413) return "Creature\\Wyvern\\Wyvern.m2"; + + auto itCachedFallback = missingDisplayFallbackPathCache_.find(displayId); + if (itCachedFallback != missingDisplayFallbackPathCache_.end()) { + return itCachedFallback->second; + } + + uint32_t bestDisplayId = 0; + uint32_t bestDelta = std::numeric_limits::max(); + std::string bestPath; + for (const auto& [candidateDisplayId, candidateData] : displayDataMap_) { + auto itCandidatePath = modelIdToPath_.find(candidateData.modelId); + if (itCandidatePath == modelIdToPath_.end() || itCandidatePath->second.empty()) { + continue; + } + uint32_t delta = (candidateDisplayId > displayId) + ? (candidateDisplayId - displayId) + : (displayId - candidateDisplayId); + if (delta < bestDelta) { + bestDelta = delta; + bestDisplayId = candidateDisplayId; + bestPath = itCandidatePath->second; + if (delta == 0) break; + } + } + if (!bestPath.empty()) { + missingDisplayFallbackPathCache_[displayId] = bestPath; + if (warnedMissingDisplayFallbackIds_.insert(displayId).second) { + LOG_WARNING("No display data for displayId ", displayId, + " — using nearest fallback displayId ", bestDisplayId, + " (delta=", bestDelta, ") path=", bestPath); + } + return bestPath; + } + if (warnedMissingDisplayDataIds_.insert(displayId).second) { LOG_WARNING("No display data for displayId ", displayId, " (displayDataMap_ has ", displayDataMap_.size(), " entries)"); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c4bfaeb3..ea00aa66 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2823,19 +2823,24 @@ void GameHandler::handlePacket(network::Packet& packet) { // In pre-world states we need full visibility (char create/login handshakes). // In-world we keep de-duplication to avoid heavy log I/O in busy areas. if (state != WorldState::IN_WORLD) { - LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec, - " state=", static_cast(state), - " size=", packet.getSize()); - const auto& data = packet.getData(); - std::string hex; - size_t limit = std::min(data.size(), 48); - hex.reserve(limit * 3); - for (size_t i = 0; i < limit; ++i) { - char b[4]; - snprintf(b, sizeof(b), "%02x ", data[i]); - hex += b; + static std::unordered_set loggedUnhandledByState; + const uint32_t key = (static_cast(static_cast(state)) << 16) | + static_cast(opcode); + if (loggedUnhandledByState.insert(key).second) { + LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec, + " state=", static_cast(state), + " size=", packet.getSize()); + const auto& data = packet.getData(); + std::string hex; + size_t limit = std::min(data.size(), 48); + hex.reserve(limit * 3); + for (size_t i = 0; i < limit; ++i) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", data[i]); + hex += b; + } + LOG_INFO("Unhandled opcode payload hex (first ", limit, " bytes): ", hex); } - LOG_INFO("Unhandled opcode payload hex (first ", limit, " bytes): ", hex); } else { static std::unordered_set loggedUnhandledOpcodes; if (loggedUnhandledOpcodes.insert(static_cast(opcode)).second) { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 59da759e..0e598125 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -126,7 +126,12 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo uint32_t pointCount = packet.readUInt32(); if (pointCount > 256) { - LOG_WARNING(" [Classic] Spline pointCount=", pointCount, " exceeds max, capping"); + static uint32_t badClassicSplineCount = 0; + ++badClassicSplineCount; + if (badClassicSplineCount <= 5 || (badClassicSplineCount % 100) == 0) { + LOG_WARNING(" [Classic] Spline pointCount=", pointCount, + " exceeds max, capping (occurrence=", badClassicSplineCount, ")"); + } pointCount = 0; } for (uint32_t i = 0; i < pointCount; i++) { @@ -1135,7 +1140,12 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc uint32_t pointCount = packet.readUInt32(); if (pointCount > 256) { - LOG_WARNING(" [Turtle] Spline pointCount=", pointCount, " exceeds max, capping"); + static uint32_t badTurtleSplineCount = 0; + ++badTurtleSplineCount; + if (badTurtleSplineCount <= 5 || (badTurtleSplineCount % 100) == 0) { + LOG_WARNING(" [Turtle] Spline pointCount=", pointCount, + " exceeds max, capping (occurrence=", badTurtleSplineCount, ")"); + } pointCount = 0; } for (uint32_t i = 0; i < pointCount; i++) { diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 1d30175e..3223ae42 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -140,7 +140,12 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& uint32_t pointCount = packet.readUInt32(); if (pointCount > 256) { - LOG_WARNING(" [TBC] Spline pointCount=", pointCount, " exceeds max, capping"); + static uint32_t badTbcSplineCount = 0; + ++badTbcSplineCount; + if (badTbcSplineCount <= 5 || (badTbcSplineCount % 100) == 0) { + LOG_WARNING(" [TBC] Spline pointCount=", pointCount, + " exceeds max, capping (occurrence=", badTbcSplineCount, ")"); + } pointCount = 0; } for (uint32_t i = 0; i < pointCount; i++) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 39b337c6..54a3dc1d 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -911,8 +911,14 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock uint32_t pointCount = packet.readUInt32(); if (pointCount > 256) { - LOG_WARNING(" Spline pointCount=", pointCount, " exceeds maximum, capping at 0 (readPos=", - packet.getReadPos(), "/", packet.getSize(), ")"); + static uint32_t badSplineCount = 0; + ++badSplineCount; + if (badSplineCount <= 5 || (badSplineCount % 100) == 0) { + LOG_WARNING(" Spline pointCount=", pointCount, + " exceeds maximum, capping at 0 (readPos=", + packet.getReadPos(), "/", packet.getSize(), + ", occurrence=", badSplineCount, ")"); + } pointCount = 0; } else { LOG_DEBUG(" Spline pointCount=", pointCount); diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 85c85730..7a3df05d 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "stb_image.h" @@ -145,13 +146,19 @@ BLPImage AssetManager::loadTexture(const std::string& path) { std::vector blpData = readFile(normalizedPath); if (blpData.empty()) { - LOG_WARNING("Texture not found: ", normalizedPath); + static std::unordered_set loggedMissingTextures; + if (loggedMissingTextures.insert(normalizedPath).second) { + LOG_WARNING("Texture not found: ", normalizedPath); + } return BLPImage(); } BLPImage image = BLPLoader::load(blpData); if (!image.isValid()) { - LOG_ERROR("Failed to load texture: ", normalizedPath); + static std::unordered_set loggedDecodeFails; + if (loggedDecodeFails.insert(normalizedPath).second) { + LOG_ERROR("Failed to load texture: ", normalizedPath); + } return BLPImage(); } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 76761167..7e206f11 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1167,7 +1167,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { GLuint texId = loadTexture(texPath, tex.flags); bool failed = (texId == whiteTexture); if (failed) { - LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", texPath); + static std::unordered_set loggedModelTextureFails; + std::string failKey = model.name + "|" + texKey; + if (loggedModelTextureFails.insert(failKey).second) { + LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", texPath); + } } if (isInvisibleTrap) { LOG_INFO(" InvisibleTrap texture[", ti, "]: ", texPath, " -> ", (failed ? "WHITE" : "OK")); @@ -1187,8 +1191,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } static const bool kGlowDiag = envFlagEnabled("WOWEE_M2_GLOW_DIAG", false); - static std::unordered_set loggedLanternGlowModels; - { + if (kGlowDiag) { std::string lowerName = model.name; std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); @@ -1196,11 +1199,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("lantern") != std::string::npos) || (lowerName.find("lamp") != std::string::npos) || (lowerName.find("light") != std::string::npos); - if (lanternLike && (kGlowDiag || loggedLanternGlowModels.insert(lowerName).second)) { + if (lanternLike) { for (size_t ti = 0; ti < model.textures.size(); ++ti) { const std::string key = (ti < textureKeysLower.size()) ? textureKeysLower[ti] : std::string(); - LOG_INFO("M2 GLOW TEX '", model.name, "' tex[", ti, "]='", key, "' flags=0x", - std::hex, model.textures[ti].flags, std::dec); + LOG_DEBUG("M2 GLOW TEX '", model.name, "' tex[", ti, "]='", key, "' flags=0x", + std::hex, model.textures[ti].flags, std::dec); } } } @@ -1386,16 +1389,16 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("light") != std::string::npos || lowerName.find("lamp") != std::string::npos || lowerName.find("lantern") != std::string::npos)) { - LOG_INFO("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(), - ": blend=", bgpu.blendMode, " matFlags=0x", - std::hex, bgpu.materialFlags, std::dec, - " colorKey=", bgpu.colorKeyBlack ? "Y" : "N", - " hasAlpha=", bgpu.hasAlpha ? "Y" : "N", - " unlit=", (bgpu.materialFlags & 0x01) ? "Y" : "N", - " lanternHint=", bgpu.lanternGlowHint ? "Y" : "N", - " glowSize=", bgpu.glowSize, - " tex=", bgpu.texture, - " idxCount=", bgpu.indexCount); + LOG_DEBUG("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(), + ": blend=", bgpu.blendMode, " matFlags=0x", + std::hex, bgpu.materialFlags, std::dec, + " colorKey=", bgpu.colorKeyBlack ? "Y" : "N", + " hasAlpha=", bgpu.hasAlpha ? "Y" : "N", + " unlit=", (bgpu.materialFlags & 0x01) ? "Y" : "N", + " lanternHint=", bgpu.lanternGlowHint ? "Y" : "N", + " glowSize=", bgpu.glowSize, + " tex=", bgpu.texture, + " idxCount=", bgpu.indexCount); } gpuModel.batches.push_back(bgpu); } @@ -3144,7 +3147,10 @@ GLuint M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { // Load BLP texture pipeline::BLPImage blp = assetManager->loadTexture(key); if (!blp.isValid()) { - LOG_WARNING("M2: Failed to load texture: ", path); + static std::unordered_set loggedTextureLoadFails; + if (loggedTextureLoadFails.insert(key).second) { + LOG_WARNING("M2: Failed to load texture: ", path); + } // Don't cache failures — transient StormLib thread contention can // cause reads to fail; next loadModel call will retry. return whiteTexture;