From a795239e77d9a2371cfe860f83f095c55b4d03b8 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 27 Mar 2026 16:51:13 -0700 Subject: [PATCH] fix: spline parse order (WotLK-first) fixes missing NPCs; bound WMO liquid loading Spline auto-detection: try WotLK format before Classic to prevent false-positive matches where durationMod float bytes resemble a valid Classic pointCount. This caused the movement block to consume wrong byte count, corrupting the update mask read (maskBlockCount=57/129/203 instead of ~5) and silently dropping NPC spawns. Terrain latency: bound WMO liquid group loading to 4 groups per advanceFinalization call. Large WMOs (e.g., Stormwind canals with 40+ liquid groups) previously loaded all groups in one unbounded loop, blowing past the 8ms frame budget and causing stalls up to 1300ms. Now yields back to processReadyTiles() after 4 groups so the time budget check can break out. --- include/rendering/terrain_manager.hpp | 1 + src/game/world_packets.cpp | 17 ++++--- src/rendering/terrain_manager.cpp | 67 ++++++++++++++++++--------- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index ab6e881f..50c09680 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -157,6 +157,7 @@ struct FinalizingTile { size_t wmoModelIndex = 0; // Next WMO model to upload size_t wmoInstanceIndex = 0; // Next WMO placement to instantiate size_t wmoDoodadIndex = 0; // Next WMO doodad to upload + size_t wmoLiquidGroupIndex = 0; // Next liquid group within current WMO instance // Incremental terrain upload state (splits TERRAIN phase across frames) bool terrainPreloaded = false; // True after preloaded textures uploaded diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 90bd292d..bf0b0afe 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1019,12 +1019,11 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock return true; }; - // --- Try 1: Classic format (uncompressed points immediately after splineId) --- - bool splineParsed = tryParseSplinePoints(false, "classic"); - - // --- Try 2: WotLK format (durationMod+durationModNext+conditional+compressed points) --- - if (!splineParsed) { - packet.setReadPos(afterSplineId); + // --- Try 1: WotLK format (durationMod+durationModNext+parabolic+compressed points) --- + // Try WotLK first since this is a WotLK parser; Classic auto-detect can false-positive + // when durationMod bytes happen to look like a valid Classic pointCount. + bool splineParsed = false; + { bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext if (wotlkOk) { /*float durationMod =*/ packet.readFloat(); @@ -1050,6 +1049,12 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock } } } + + // --- Try 2: Classic format (uncompressed points immediately after splineId) --- + if (!splineParsed) { + packet.setReadPos(afterSplineId); + splineParsed = tryParseSplinePoints(false, "classic"); + } } } else if (updateFlags & UPDATEFLAG_POSITION) { diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index ca2b6a1b..8ec00b92 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -980,43 +980,68 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { case FinalizationPhase::WMO_INSTANCES: { // Create WMO instances incrementally to avoid stalls on tiles with many WMOs. + // Liquid group loading is also budgeted (max 4 per call) to prevent stalls + // on WMOs with many liquid groups (e.g. Stormwind canals). if (wmoRenderer && ft.wmoInstanceIndex < pending->wmoModels.size()) { constexpr size_t kWmoInstancesPerStep = 4; + constexpr size_t kLiquidGroupsPerStep = 4; size_t created = 0; + size_t liquidGroupsLoaded = 0; while (ft.wmoInstanceIndex < pending->wmoModels.size() && created < kWmoInstancesPerStep) { - auto& wmoReady = pending->wmoModels[ft.wmoInstanceIndex++]; + auto& wmoReady = pending->wmoModels[ft.wmoInstanceIndex]; + // Skip duplicates and unloaded models if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { + ft.wmoInstanceIndex++; + ft.wmoLiquidGroupIndex = 0; continue; } if (!wmoRenderer->isModelLoaded(wmoReady.modelId)) { + ft.wmoInstanceIndex++; + ft.wmoLiquidGroupIndex = 0; continue; } - uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); - if (wmoInstId) { + // Create the instance on first visit (liquidGroupIndex == 0) + if (ft.wmoLiquidGroupIndex == 0) { + uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); + if (!wmoInstId) { + ft.wmoInstanceIndex++; + continue; + } ft.wmoInstanceIds.push_back(wmoInstId); if (wmoReady.uniqueId != 0) { placedWmoIds.insert(wmoReady.uniqueId); ft.tileWmoUniqueIds.push_back(wmoReady.uniqueId); } - // Load WMO liquids (canals, pools, etc.) - if (waterRenderer) { - glm::mat4 modelMatrix = glm::mat4(1.0f); - modelMatrix = glm::translate(modelMatrix, wmoReady.position); - modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); - modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); - modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); - for (const auto& group : wmoReady.model.groups) { - if (!group.liquid.hasLiquid()) continue; - if (group.flags & 0x2000) { - uint16_t lt = group.liquid.materialId; - uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4); - if (basicType < 2) continue; - } - waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId); - } - } - created++; } + // Load WMO liquids incrementally (canals, pools, etc.) + if (waterRenderer) { + uint32_t wmoInstId = ft.wmoInstanceIds.back(); + glm::mat4 modelMatrix = glm::mat4(1.0f); + modelMatrix = glm::translate(modelMatrix, wmoReady.position); + modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); + modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); + const auto& groups = wmoReady.model.groups; + while (ft.wmoLiquidGroupIndex < groups.size() && liquidGroupsLoaded < kLiquidGroupsPerStep) { + const auto& group = groups[ft.wmoLiquidGroupIndex]; + ft.wmoLiquidGroupIndex++; + if (!group.liquid.hasLiquid()) continue; + if (group.flags & 0x2000) { + uint16_t lt = group.liquid.materialId; + uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4); + if (basicType < 2) continue; + } + waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId); + liquidGroupsLoaded++; + } + // More liquid groups remain on this WMO — yield + if (ft.wmoLiquidGroupIndex < groups.size()) { + return false; + } + } + ft.wmoInstanceIndex++; + ft.wmoLiquidGroupIndex = 0; + created++; } if (ft.wmoInstanceIndex < pending->wmoModels.size()) { return false; // More WMO instances to create — yield