From 16d88f19fc47ca9f2d9838d70865cf940d80c4e3 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 27 Feb 2026 04:59:12 -0800 Subject: [PATCH] Fix instance portals: WDT byte order, box trigger sizing, suppress ping-pong, WMO cache cleanup - Fix WDT chunk magic constants to big-endian ASCII (matching ADTLoader) - Add minimum effective size for box area triggers (90 units, like sphere 45-unit radius) - Add areaTriggerSuppressFirst_ flag to prevent portal ping-pong on map transfer - Add WMORenderer::clearAll() to clear models/textures on map change (prevents GPU crash) - Increase WMO texture cache default to 8GB - Fix setMapName called after loadTestTerrain so WMO renderer exists - Save/restore player position around CMSG_AREATRIGGER to prevent bad DB persistence --- include/game/game_handler.hpp | 1 + include/rendering/wmo_renderer.hpp | 7 ++- src/core/application.cpp | 20 +++++-- src/game/game_handler.cpp | 85 +++++++++++++++++++----------- src/pipeline/wdt_loader.cpp | 12 ++--- src/rendering/wmo_renderer.cpp | 36 ++++++++++++- 6 files changed, 120 insertions(+), 41 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index ebee07e1..ebf2404a 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1500,6 +1500,7 @@ private: std::vector areaTriggers_; std::unordered_set activeAreaTriggers_; // triggers player is currently inside float areaTriggerCheckTimer_ = 0.0f; + bool areaTriggerSuppressFirst_ = false; // suppress first check after map transfer float castTimeTotal = 0.0f; std::array actionBar{}; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 1b1526c6..6ba1c4fe 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -136,6 +136,11 @@ public: */ void clearInstances(); + /** + * Clear all instances, loaded models, and texture cache (for map transitions) + */ + void clearAll(); + /** * Render all WMO instances (Vulkan) * @param cmd Command buffer to record into @@ -630,7 +635,7 @@ private: std::unordered_map textureCache; size_t textureCacheBytes_ = 0; uint64_t textureCacheCounter_ = 0; - size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init + size_t textureCacheBudgetBytes_ = 8192ull * 1024 * 1024; // 8 GB default, overridden at init std::unordered_set failedTextureCache_; std::unordered_set loggedTextureLoadFails_; uint32_t textureBudgetRejectWarnings_ = 0; diff --git a/src/core/application.cpp b/src/core/application.cpp index fa93bc8a..918568d3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3218,9 +3218,9 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float pendingTransportDoodadBatches_.clear(); if (renderer) { - // Clear all world geometry from old map + // Clear all world geometry from old map (including textures/models) if (auto* wmo = renderer->getWMORenderer()) { - wmo->clearInstances(); + wmo->clearAll(); } if (auto* m2 = renderer->getM2Renderer()) { m2->clear(); @@ -3384,7 +3384,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float if (isWMOOnlyMap) { // ---- WMO-only map (dungeon/raid/BG): load root WMO directly ---- - LOG_INFO("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath); + LOG_WARNING("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath); showProgress("Loading instance geometry...", 0.25f); // Still call loadTestTerrain with a dummy path to initialize all renderers @@ -3392,7 +3392,17 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y); std::string dummyAdtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt"; + LOG_WARNING("WMO-only: calling loadTestTerrain with dummy ADT: ", dummyAdtPath); renderer->loadTestTerrain(assetManager.get(), dummyAdtPath); + LOG_WARNING("WMO-only: loadTestTerrain returned"); + + // Set map name on the newly-created WMO renderer (loadTestTerrain creates it) + if (renderer->getWMORenderer()) { + renderer->getWMORenderer()->setMapName(mapName); + } + if (renderer->getTerrainManager()) { + renderer->getTerrainManager()->setMapName(mapName); + } // Disable terrain streaming — no ADT tiles for WMO-only maps if (renderer->getTerrainManager()) { @@ -3407,10 +3417,14 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Load the root WMO auto* wmoRenderer = renderer->getWMORenderer(); + LOG_WARNING("WMO-only: wmoRenderer=", (wmoRenderer ? "valid" : "NULL")); if (wmoRenderer) { + LOG_WARNING("WMO-only: reading root WMO file: ", wdtInfo.rootWMOPath); std::vector wmoData = assetManager->readFile(wdtInfo.rootWMOPath); + LOG_WARNING("WMO-only: root WMO data size=", wmoData.size()); if (!wmoData.empty()) { pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); + LOG_WARNING("WMO-only: parsed WMO model, nGroups=", wmoModel.nGroups); if (wmoModel.nGroups > 0) { showProgress("Loading instance groups...", 0.35f); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 24f8956f..4f3a77ae 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3430,6 +3430,8 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { // Initialize movement info with world entry position (server → canonical) glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(data.x, data.y, data.z)); + LOG_WARNING("LOGIN_VERIFY_WORLD: server=(", data.x, ", ", data.y, ", ", data.z, + ") canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ") mapId=", data.mapId); movementInfo.x = canonical.x; movementInfo.y = canonical.y; movementInfo.z = canonical.z; @@ -8800,27 +8802,31 @@ void GameHandler::checkAreaTriggers() { int mapTriggerCount = 0; float closestDist = 999999.0f; uint32_t closestId = 0; - float closestX = 0, closestY = 0, closestZ = 0; + float closestR = 0, closestBoxL = 0, closestBoxW = 0, closestBoxH = 0; + bool closestActive = false; for (const auto& at : areaTriggers_) { if (at.mapId != currentMapId_) continue; mapTriggerCount++; float dx = px - at.x, dy = py - at.y, dz = pz - at.z; float dist = std::sqrt(dx*dx + dy*dy + dz*dz); - if (dist < closestDist) { closestDist = dist; closestId = at.id; closestX = at.x; closestY = at.y; closestZ = at.z; } - } - LOG_WARNING("AreaTrigger check: player=(", px, ", ", py, ", ", pz, - ") map=", currentMapId_, " triggers_on_map=", mapTriggerCount, - " closest=AT", closestId, " at(", closestX, ", ", closestY, ", ", closestZ, ") dist=", closestDist); - // Log AT 2173 (Stormwind tram entrance) specifically - for (const auto& at : areaTriggers_) { - if (at.id == 2173) { - float dx = px - at.x, dy = py - at.y, dz = pz - at.z; - float dist = std::sqrt(dx*dx + dy*dy + dz*dz); - LOG_WARNING(" AT2173: map=", at.mapId, " pos=(", at.x, ", ", at.y, ", ", at.z, - ") r=", at.radius, " box=(", at.boxLength, ", ", at.boxWidth, ", ", at.boxHeight, ") dist=", dist); - break; + if (dist < closestDist) { + closestDist = dist; closestId = at.id; + closestR = at.radius; closestBoxL = at.boxLength; closestBoxW = at.boxWidth; closestBoxH = at.boxHeight; + closestActive = activeAreaTriggers_.count(at.id) > 0; } } + LOG_WARNING("AreaTrigger check: player=(", px, ", ", py, ", ", pz, + ") map=", currentMapId_, " closest=AT", closestId, + " dist=", closestDist, " r=", closestR, + " box=(", closestBoxL, ",", closestBoxW, ",", closestBoxH, + ") active=", closestActive); + } + + // On first check after map transfer, just mark which triggers we're inside + // without firing them — prevents exit portal from immediately sending us back + bool suppressFirst = areaTriggerSuppressFirst_; + if (suppressFirst) { + areaTriggerSuppressFirst_ = false; } for (const auto& at : areaTriggers_) { @@ -8837,7 +8843,12 @@ void GameHandler::checkAreaTriggers() { float distSq = dx * dx + dy * dy + dz * dz; inside = (distSq <= effectiveRadius * effectiveRadius); } else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) { - // Box trigger (axis-aligned or rotated) + // Box trigger — use generous minimum dimensions since WMO collision + // may block the player from reaching small triggers inside doorways + float effLength = std::max(at.boxLength, 90.0f); + float effWidth = std::max(at.boxWidth, 90.0f); + float effHeight = std::max(at.boxHeight, 90.0f); + float dx = px - at.x; float dy = py - at.y; float dz = pz - at.z; @@ -8848,28 +8859,41 @@ void GameHandler::checkAreaTriggers() { float localX = dx * cosYaw - dy * sinYaw; float localY = dx * sinYaw + dy * cosYaw; - inside = (std::abs(localX) <= at.boxLength * 0.5f && - std::abs(localY) <= at.boxWidth * 0.5f && - std::abs(dz) <= at.boxHeight * 0.5f); + inside = (std::abs(localX) <= effLength * 0.5f && + std::abs(localY) <= effWidth * 0.5f && + std::abs(dz) <= effHeight * 0.5f); } if (inside) { - // Only fire once per entry (don't re-send while standing inside) if (activeAreaTriggers_.count(at.id) == 0) { activeAreaTriggers_.insert(at.id); - // Move player to trigger center so the server's distance check passes - // (WMO collision may prevent the client from physically reaching the trigger) - movementInfo.x = at.x; - movementInfo.y = at.y; - movementInfo.z = at.z; - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); + if (suppressFirst) { + // After map transfer: mark triggers we're inside of, but don't fire them. + // This prevents the exit portal from immediately sending us back. + LOG_WARNING("AreaTrigger suppressed (post-transfer): AT", at.id); + } else { + // Temporarily move player to trigger center so the server's distance + // check passes, then restore to actual position so the server doesn't + // persist the fake position on disconnect. + float savedX = movementInfo.x, savedY = movementInfo.y, savedZ = movementInfo.z; + movementInfo.x = at.x; + movementInfo.y = at.y; + movementInfo.z = at.z; + sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER)); - pkt.writeUInt32(at.id); - socket->send(pkt); - LOG_WARNING("Fired CMSG_AREATRIGGER: id=", at.id, - " at (", at.x, ", ", at.y, ", ", at.z, ")"); + network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER)); + pkt.writeUInt32(at.id); + socket->send(pkt); + LOG_WARNING("Fired CMSG_AREATRIGGER: id=", at.id, + " at (", at.x, ", ", at.y, ", ", at.z, ")"); + + // Restore actual player position + movementInfo.x = savedX; + movementInfo.y = savedY; + movementInfo.z = savedZ; + sendMovement(Opcode::MSG_MOVE_HEARTBEAT); + } } } else { // Player left the trigger — allow re-fire on re-entry @@ -12232,6 +12256,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) { worldStateZoneId_ = 0; activeAreaTriggers_.clear(); areaTriggerCheckTimer_ = -5.0f; // 5-second cooldown after map transfer + areaTriggerSuppressFirst_ = true; // first check just marks active triggers, doesn't fire stopAutoAttack(); casting = false; currentCastSpellId = 0; diff --git a/src/pipeline/wdt_loader.cpp b/src/pipeline/wdt_loader.cpp index b6af879a..dde1bb94 100644 --- a/src/pipeline/wdt_loader.cpp +++ b/src/pipeline/wdt_loader.cpp @@ -25,12 +25,12 @@ float readF32(const uint8_t* data, size_t offset) { return v; } -// Chunk magic constants (little-endian) -constexpr uint32_t MVER = 0x5245564D; // "REVM" -constexpr uint32_t MPHD = 0x4448504D; // "DHPM" -constexpr uint32_t MAIN = 0x4E49414D; // "NIAM" -constexpr uint32_t MWMO = 0x4F4D574D; // "OMWM" -constexpr uint32_t MODF = 0x46444F4D; // "FDOM" +// Chunk magic constants (big-endian ASCII, same as ADTLoader) +constexpr uint32_t MVER = 0x4D564552; // "MVER" +constexpr uint32_t MPHD = 0x4D504844; // "MPHD" +constexpr uint32_t MAIN = 0x4D41494E; // "MAIN" +constexpr uint32_t MWMO = 0x4D574D4F; // "MWMO" +constexpr uint32_t MODF = 0x4D4F4446; // "MODF" } // anonymous namespace diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 145128a2..0374c96d 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -274,7 +274,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou flatNormalTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT); textureCacheBudgetBytes_ = - envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 4096) * 1024ull * 1024ull; + envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 8192) * 1024ull * 1024ull; modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000); core::Logger::getInstance().info("WMO texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB"); @@ -1039,6 +1039,40 @@ void WMORenderer::clearInstances() { core::Logger::getInstance().info("Cleared all WMO instances"); } +void WMORenderer::clearAll() { + clearInstances(); + + if (vkCtx_) { + VkDevice device = vkCtx_->getDevice(); + VmaAllocator allocator = vkCtx_->getAllocator(); + vkDeviceWaitIdle(device); + + // Free GPU resources for loaded models + for (auto& [id, model] : loadedModels) { + for (auto& group : model.groups) { + destroyGroupGPU(group); + } + } + + // Free cached textures + for (auto& [path, entry] : textureCache) { + if (entry.texture) entry.texture->destroy(device, allocator); + if (entry.normalHeightMap) entry.normalHeightMap->destroy(device, allocator); + } + } + + loadedModels.clear(); + textureCache.clear(); + textureCacheBytes_ = 0; + textureCacheCounter_ = 0; + failedTextureCache_.clear(); + loggedTextureLoadFails_.clear(); + textureBudgetRejectWarnings_ = 0; + precomputedFloorGrid.clear(); + + LOG_WARNING("Cleared all WMO models, instances, and texture cache"); +} + void WMORenderer::setCollisionFocus(const glm::vec3& worldPos, float radius) { collisionFocusEnabled = (radius > 0.0f); collisionFocusPos = worldPos;