From 01fecbf3e0007307e34567b12fd73085e86265ef Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 14 Apr 2026 06:06:50 -0700 Subject: [PATCH] fix(parsing): correct UPDATE_OBJECT PackedGuid, cape textures, and missing asset guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix MOVEMENT update type to use readPackedGuid() instead of readUInt64() (WotLK 3.3.5a) - Add desync diagnostic logging to UPDATE_OBJECT parser for future debugging - Register MSG_MOVE_SET_COLLISION_HGT (0x518) as skip handler - Fix cape texture lookup to only try .blp extension variants (4 files) - Add fileExists() guards for underwear textures referencing missing BLP files (4 files) - Add spell visual impact→cast M2 path fallback - Skip WMO doodad instance creation when model load fails - Demote spell caster position warning to debug level --- src/core/appearance_composer.cpp | 4 +-- src/core/entity_spawner.cpp | 25 ++++++++------- src/core/entity_spawner_player.cpp | 27 ++++++++++++---- src/game/game_handler_packets.cpp | 2 ++ src/game/spell_handler.cpp | 2 +- src/game/world_packets.cpp | 46 +++++++++++++++++++++++++-- src/rendering/character_preview.cpp | 15 +++++---- src/rendering/spell_visual_system.cpp | 15 +++++---- src/rendering/terrain_manager.cpp | 7 +++- 9 files changed, 105 insertions(+), 38 deletions(-) diff --git a/src/core/appearance_composer.cpp b/src/core/appearance_composer.cpp index d7d38cc2..218be3f4 100644 --- a/src/core/appearance_composer.cpp +++ b/src/core/appearance_composer.cpp @@ -123,12 +123,12 @@ PlayerTextureInfo AppearanceComposer::resolvePlayerTextures(pipeline::M2Model& m else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) { for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); - if (!tex.empty()) { + if (!tex.empty() && assetManager_->fileExists(tex)) { result.underwearPaths.push_back(tex); LOG_INFO(" DBC underwear texture: ", tex); } } - foundUnderwear = true; + foundUnderwear = !result.underwearPaths.empty(); } if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break; diff --git a/src/core/entity_spawner.cpp b/src/core/entity_spawner.cpp index a904c048..1b17354c 100644 --- a/src/core/entity_spawner.cpp +++ b/src/core/entity_spawner.cpp @@ -1069,7 +1069,8 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float } else if (section == 4 && npcUnderwear.empty() && color == npcSkin) { for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = csDbc->getString(r, f); - if (!tex.empty()) npcUnderwear.push_back(tex); + if (!tex.empty() && am->fileExists(tex)) + npcUnderwear.push_back(tex); } } } @@ -1659,14 +1660,15 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float const bool hasDir = (name.find('\\') != std::string::npos); const bool hasExt = hasBlpExt(name); if (hasDir) { - addCapeCandidate(name); - if (!hasExt) addCapeCandidate(name + ".blp"); + if (hasExt) addCapeCandidate(name); + else addCapeCandidate(name + ".blp"); } else { std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name; std::string baseTex = "Item\\TextureComponents\\Cape\\" + name; - addCapeCandidate(baseObj); - addCapeCandidate(baseTex); - if (!hasExt) { + if (hasExt) { + addCapeCandidate(baseObj); + addCapeCandidate(baseTex); + } else { addCapeCandidate(baseObj + ".blp"); addCapeCandidate(baseTex + ".blp"); } @@ -2055,14 +2057,15 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float const bool hasDir = (name.find('\\') != std::string::npos); const bool hasExt = hasBlpExt(name); if (hasDir) { - addCandidate(name); - if (!hasExt) addCandidate(name + ".blp"); + if (hasExt) addCandidate(name); + else addCandidate(name + ".blp"); } else { std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name; std::string baseTex = "Item\\TextureComponents\\Cape\\" + name; - addCandidate(baseObj); - addCandidate(baseTex); - if (!hasExt) { + if (hasExt) { + addCandidate(baseObj); + addCandidate(baseTex); + } else { addCandidate(baseObj + ".blp"); addCandidate(baseTex + ".blp"); } diff --git a/src/core/entity_spawner_player.cpp b/src/core/entity_spawner_player.cpp index 6a564dc9..8ba14b77 100644 --- a/src/core/entity_spawner_player.cpp +++ b/src/core/entity_spawner_player.cpp @@ -228,11 +228,23 @@ void EntitySpawner::spawnOnlinePlayer(uint64_t guid, hairTexturePath = charSectionsDbc->getString(r, csF.texture1); if (!hairTexturePath.empty()) foundHair = true; } else if (baseSection == 4 && !foundUnderwear && colorIndex == skinId) { + // Verify textures exist — some DBC entries reference BLPs + // that were never shipped (e.g. Draenei skin colors 10-16). + bool allExist = true; + std::vector candidateUW; for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); - if (!tex.empty()) underwearPaths.push_back(tex); + if (!tex.empty()) { + if (assetManager_->fileExists(tex)) + candidateUW.push_back(tex); + else + allExist = false; + } + } + if (allExist || !candidateUW.empty()) { + underwearPaths = std::move(candidateUW); + foundUnderwear = true; } - foundUnderwear = true; } else if (baseSection == 1 && !foundFaceLower && variationIndex == faceId && colorIndex == skinId) { std::string tex1 = charSectionsDbc->getString(r, csF.texture1); @@ -694,14 +706,15 @@ void EntitySpawner::setOnlinePlayerEquipment(uint64_t guid, }; if (hasDir) { - addCapeCandidate(capeName); - if (!hasExt) addCapeCandidate(capeName + ".blp"); + if (hasExt) addCapeCandidate(capeName); + else addCapeCandidate(capeName + ".blp"); } else { std::string baseObj = "Item\\ObjectComponents\\Cape\\" + capeName; std::string baseTex = "Item\\TextureComponents\\Cape\\" + capeName; - addCapeCandidate(baseObj); - addCapeCandidate(baseTex); - if (!hasExt) { + if (hasExt) { + addCapeCandidate(baseObj); + addCapeCandidate(baseTex); + } else { addCapeCandidate(baseObj + ".blp"); addCapeCandidate(baseTex + ".blp"); } diff --git a/src/game/game_handler_packets.cpp b/src/game/game_handler_packets.cpp index 84b65727..5f8d062a 100644 --- a/src/game/game_handler_packets.cpp +++ b/src/game/game_handler_packets.cpp @@ -2523,6 +2523,8 @@ void GameHandler::registerOpcodeHandlers() { }; // GM ticket status (new/updated); no ticket UI yet registerSkipHandler(Opcode::SMSG_GM_TICKET_STATUS_UPDATE); + // Broadcast of another player's collision height change — cosmetic only. + registerSkipHandler(Opcode::MSG_MOVE_SET_COLLISION_HGT); // Client uses this outbound; treat inbound variant as no-op for robustness. registerSkipHandler(Opcode::MSG_MOVE_WORLDPORT_ACK); // Observed custom server packet (8 bytes). Safe-consume for now. diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 72476c3c..dcba17b5 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -120,7 +120,7 @@ void SpellHandler::triggerCastVisual(uint32_t spellId, uint64_t casterGuid, uint uint32_t visualId = resolveSpellVisualId(spellId); if (visualId == 0) { LOG_WARNING("SpellVisual: triggerCastVisual — visualId=0 for spellId=", spellId); return; } glm::vec3 casterPos; - if (!resolveUnitPosition(casterGuid, casterPos)) { LOG_WARNING("SpellVisual: triggerCastVisual — cannot resolve caster position"); return; } + if (!resolveUnitPosition(casterGuid, casterPos)) { LOG_DEBUG("SpellVisual: triggerCastVisual — cannot resolve caster position for guid=0x", std::hex, casterGuid, std::dec); return; } LOG_INFO("SpellVisual: triggerCastVisual visualId=", visualId, " pos=(", casterPos.x, ",", casterPos.y, ",", casterPos.z, ") castTimeMs=", castTimeMs); svs->playSpellVisualPrecast(visualId, casterPos, castTimeMs); } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index b40d8b12..407908f2 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1183,9 +1183,9 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& } case UpdateType::MOVEMENT: { - // Movement update - if (!packet.hasRemaining(8)) return false; - block.guid = packet.readUInt64(); + // Movement update — WotLK 3.3.5a uses PackedGuid (NOT full uint64) + if (!packet.hasData()) return false; + block.guid = packet.readPackedGuid(); LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec); return parseMovementBlock(packet, block); @@ -1288,9 +1288,18 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) data.blockCount = remainingBlockCount; data.blocks.reserve(data.blockCount); + // Track last block state for desync diagnostics + uint8_t prevUpdateType = 0; + uint8_t prevObjectType = 0; + uint16_t prevUpdateFlags = 0; + uint32_t prevMoveFlags = 0; + uint64_t prevGuid = 0; + size_t prevReadPos = packet.getReadPos(); + for (uint32_t i = 0; i < data.blockCount; ++i) { LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount); + size_t blockStartPos = packet.getReadPos(); UpdateBlock block; if (!parseUpdateBlock(packet, block)) { static int parseBlockErrors = 0; @@ -1299,6 +1308,31 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) LOG_ERROR("Failed to parse update block ", i + 1, " of ", data.blockCount, " (", i, " blocks parsed, ", lostBlocks, " blocks LOST", ", remaining=", packet.getRemainingSize(), " bytes)"); + LOG_ERROR(" blockStartPos=", blockStartPos, " packetSize=", packet.getSize()); + if (i > 0) { + LOG_ERROR(" prevBlock: type=", static_cast(prevUpdateType), + " objType=", static_cast(prevObjectType), + " updateFlags=0x", std::hex, prevUpdateFlags, + " moveFlags=0x", prevMoveFlags, + " guid=0x", prevGuid, std::dec, + " startPos=", prevReadPos, + " consumed=", blockStartPos - prevReadPos, " bytes"); + } + // Peek at the failing byte(s) for format diagnosis + packet.setReadPos(blockStartPos); + uint8_t peekBytes[8] = {}; + size_t peekCount = std::min(8, packet.getRemainingSize()); + for (size_t p = 0; p < peekCount; ++p) + peekBytes[p] = packet.readUInt8(); + LOG_ERROR(" failBytes: ", + std::hex, static_cast(peekBytes[0]), " ", + static_cast(peekBytes[1]), " ", + static_cast(peekBytes[2]), " ", + static_cast(peekBytes[3]), " ", + static_cast(peekBytes[4]), " ", + static_cast(peekBytes[5]), " ", + static_cast(peekBytes[6]), " ", + static_cast(peekBytes[7]), std::dec); if (parseBlockErrors == 10) LOG_ERROR("(suppressing further update block parse errors)"); } @@ -1307,6 +1341,12 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) break; } + prevUpdateType = static_cast(block.updateType); + prevObjectType = static_cast(block.objectType); + prevUpdateFlags = block.updateFlags; + prevMoveFlags = block.moveFlags; + prevGuid = block.guid; + prevReadPos = blockStartPos; data.blocks.emplace_back(std::move(block)); } diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index f8666fcd..8fc891eb 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -443,11 +443,11 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, variationIndex == 0 && colorIndex == static_cast(skin)) { for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); - if (!tex.empty()) { + if (!tex.empty() && assetManager_->fileExists(tex)) { underwearPaths.push_back(tex); } } - foundUnderwear = true; + foundUnderwear = !underwearPaths.empty(); } } @@ -824,14 +824,15 @@ bool CharacterPreview::applyEquipment(const std::vector& eq bool hasDir = (name.find('\\') != std::string::npos); bool hasExt = hasBlpExt(name); if (hasDir) { - addCandidate(name); - if (!hasExt) addCandidate(name + ".blp"); + if (hasExt) addCandidate(name); + else addCandidate(name + ".blp"); } else { std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name; std::string baseTex = "Item\\TextureComponents\\Cape\\" + name; - addCandidate(baseObj); - addCandidate(baseTex); - if (!hasExt) { + if (hasExt) { + addCandidate(baseObj); + addCandidate(baseTex); + } else { addCandidate(baseObj + ".blp"); addCandidate(baseTex + ".blp"); } diff --git a/src/rendering/spell_visual_system.cpp b/src/rendering/spell_visual_system.cpp index 867919dc..8c457854 100644 --- a/src/rendering/spell_visual_system.cpp +++ b/src/rendering/spell_visual_system.cpp @@ -370,12 +370,15 @@ void SpellVisualSystem::playSpellVisual(uint32_t visualId, const glm::vec3& worl if (!spellVisualDbcLoaded_) loadSpellVisualDbc(); - // Select cast or impact path map - auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_; - auto pathIt = pathMap.find(visualId); - if (pathIt == pathMap.end()) { - LOG_WARNING("SpellVisual: no ", (useImpactKit ? "impact" : "cast"), " path for visualId=", visualId); - return; + // Select cast or impact path map; fall back to the other if missing + auto& primaryMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_; + auto& fallbackMap = useImpactKit ? spellVisualCastPath_ : spellVisualImpactPath_; + auto pathIt = primaryMap.find(visualId); + if (pathIt == primaryMap.end()) { + pathIt = fallbackMap.find(visualId); + if (pathIt == fallbackMap.end()) { + return; + } } const std::string& modelPath = pathIt->second; diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index d5c5a51d..6bc3338e 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -1055,7 +1055,12 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { size_t uploaded = 0; while (ft.wmoDoodadIndex < pending->wmoDoodads.size() && uploaded < kDoodadsPerStep) { auto& doodad = pending->wmoDoodads[ft.wmoDoodadIndex]; - if (m2Renderer->loadModel(doodad.model, doodad.modelId)) { + if (!m2Renderer->loadModel(doodad.model, doodad.modelId)) { + ft.wmoDoodadIndex++; + uploaded++; + continue; + } + { std::lock_guard lock(uploadedM2IdsMutex_); uploadedM2Ids_.insert(doodad.modelId); }