From 046d4615eaeb4bfd6d4b640ddc3de3902f9506ad Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Feb 2026 14:17:04 -0800 Subject: [PATCH] Fix M2 texture loading, /unstuckgy, and WMO floor detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add mutex to AssetManager::loadTexture/loadDBC/fileExists to prevent StormLib thread-safety races that silently fail texture reads; stop caching texture load failures so transient errors are retried. - Replace /unstuckgy DBC lookup (which used wrong coordinate transform) with hardcoded safe locations per map. - Widen WMO floor raycast from single grid cell to ±1 unit range query to catch bridge/walkway triangles at cell boundaries. - Tighten swept collision hit threshold (0.5 → 0.15) and grid query margin (2.5 → 1.5) to prevent false-positive wall pushes. - Tighten post-wall-push Z snap lower bound (-1.0 → -0.3) to prevent gradual floor sinking. --- src/core/application.cpp | 46 ++++++++--------------------- src/pipeline/asset_manager.cpp | 17 ++++++++--- src/rendering/camera_controller.cpp | 2 +- src/rendering/m2_renderer.cpp | 3 +- src/rendering/wmo_renderer.cpp | 23 +++++++++------ 5 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index fa870cb7..9cfd458b 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -613,42 +613,22 @@ void Application::setupUICallbacks() { auto* ft = cc->getFollowTargetMutable(); if (!ft) return; - auto wsl = assetManager->loadDBC("WorldSafeLocs.dbc"); - if (!wsl || !wsl->isLoaded()) { - LOG_WARNING("WorldSafeLocs.dbc not available for /unstuckgy"); - return; - } - - // Use current map and position. + // Hardcoded safe locations per map (canonical WoW coords) uint32_t mapId = gameHandler ? gameHandler->getCurrentMapId() : 0; - glm::vec3 cur = *ft; - float bestDist2 = std::numeric_limits::max(); - glm::vec3 bestPos = cur; - - for (uint32_t i = 0; i < wsl->getRecordCount(); i++) { - uint32_t recMap = wsl->getUInt32(i, 1); - if (recMap != mapId) continue; - float x = wsl->getFloat(i, 2); - float y = wsl->getFloat(i, 3); - float z = wsl->getFloat(i, 4); - glm::vec3 glPos = core::coords::adtToWorld(x, y, z); - float dx = glPos.x - cur.x; - float dy = glPos.y - cur.y; - float dz = glPos.z - cur.z; - float d2 = dx*dx + dy*dy + dz*dz; - if (d2 < bestDist2) { - bestDist2 = d2; - bestPos = glPos; - } + glm::vec3 safeCanonical; + switch (mapId) { + case 0: safeCanonical = glm::vec3(-8833.38f, 628.63f, 94.0f); break; // Stormwind Trade District + case 1: safeCanonical = glm::vec3(1629.36f, -4373.34f, 31.2f); break; // Orgrimmar + case 530: safeCanonical = glm::vec3(-3961.64f, -13931.2f, 100.6f); break; // Shattrath + case 571: safeCanonical = glm::vec3(5804.14f, 624.77f, 647.8f); break; // Dalaran + default: + LOG_WARNING("No hardcoded safe location for map ", mapId); + return; } - if (bestDist2 == std::numeric_limits::max()) { - LOG_WARNING("No graveyard found on map ", mapId); - return; - } - - *ft = bestPos; - cc->setDefaultSpawn(bestPos, cc->getYaw(), cc->getPitch()); + glm::vec3 safePos = core::coords::canonicalToRender(safeCanonical); + *ft = safePos; + cc->setDefaultSpawn(safePos, cc->getYaw(), cc->getPitch()); cc->reset(); }); diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index fe90f381..ea12e8fa 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -54,8 +54,12 @@ BLPImage AssetManager::loadTexture(const std::string& path) { LOG_DEBUG("Loading texture: ", normalizedPath); - // Read BLP file from MPQ - std::vector blpData = mpqManager.readFile(normalizedPath); + // Read BLP file from MPQ (must hold readMutex — StormLib is not thread-safe) + std::vector blpData; + { + std::lock_guard lock(readMutex); + blpData = mpqManager.readFile(normalizedPath); + } if (blpData.empty()) { LOG_WARNING("Texture not found: ", normalizedPath); return BLPImage(); @@ -90,8 +94,12 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { // Construct DBC path (DBFilesClient directory) std::string dbcPath = "DBFilesClient\\" + name; - // Read DBC file from MPQ - std::vector dbcData = mpqManager.readFile(dbcPath); + // Read DBC file from MPQ (must hold readMutex — StormLib is not thread-safe) + std::vector dbcData; + { + std::lock_guard lock(readMutex); + dbcData = mpqManager.readFile(dbcPath); + } if (dbcData.empty()) { LOG_WARNING("DBC not found: ", dbcPath); return nullptr; @@ -124,6 +132,7 @@ bool AssetManager::fileExists(const std::string& path) const { return false; } + std::lock_guard lock(readMutex); return mpqManager.fileExists(normalizePath(path)); } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 0605ff51..15768bc2 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -481,7 +481,7 @@ void CameraController::update(float deltaTime) { candidate.y = adjusted.y; // Snap Z to floor at adjusted position to prevent fall-through auto adjFloor = wmoRenderer->getFloorHeight(adjusted.x, adjusted.y, feetZ + 2.5f); - if (adjFloor && *adjFloor >= feetZ - 1.0f && *adjFloor <= feetZ + 1.6f) { + if (adjFloor && *adjFloor >= feetZ - 0.3f && *adjFloor <= feetZ + 1.6f) { candidate.z = *adjFloor; } } else if (floorH && *floorH > candidate.z) { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index d74c7d79..494cbc8e 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2307,7 +2307,8 @@ GLuint M2Renderer::loadTexture(const std::string& path) { pipeline::BLPImage blp = assetManager->loadTexture(path); if (!blp.isValid()) { LOG_WARNING("M2: Failed to load texture: ", path); - textureCache[path] = whiteTexture; + // Don't cache failures — transient StormLib thread contention can + // cause reads to fail; next loadModel call will retry. return whiteTexture; } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 950768c2..be9b726d 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1736,10 +1736,15 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ const auto& verts = group.collisionVertices; const auto& indices = group.collisionIndices; - // Use spatial grid to only test triangles near the query XY - const auto* cellTris = group.getTrianglesAtLocal(localOrigin.x, localOrigin.y); - if (cellTris) { - for (uint32_t triStart : *cellTris) { + // Use spatial grid to test triangles near the query XY. + // Query a small range (±1 unit) to catch floor triangles at cell boundaries + // (bridges, narrow walkways whose triangles may sit in adjacent cells). + group.getTrianglesInRange( + localOrigin.x - 1.0f, localOrigin.y - 1.0f, + localOrigin.x + 1.0f, localOrigin.y + 1.0f, + wallTriScratch); + { + for (uint32_t triStart : wallTriScratch) { const glm::vec3& v0 = verts[indices[triStart]]; const glm::vec3& v1 = verts[indices[triStart + 1]]; const glm::vec3& v2 = verts[indices[triStart + 2]]; @@ -1859,10 +1864,10 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, const auto& indices = group.collisionIndices; // Use spatial grid: query range covering the movement segment + player radius - float rangeMinX = std::min(localFrom.x, localTo.x) - PLAYER_RADIUS - 2.5f; - float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 2.5f; - float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 2.5f; - float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 2.5f; + float rangeMinX = std::min(localFrom.x, localTo.x) - PLAYER_RADIUS - 1.5f; + float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.5f; + float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.5f; + float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.5f; group.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, wallTriScratch); for (uint32_t triStart : wallTriScratch) { @@ -1913,7 +1918,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3 hitPoint = localFrom + (localTo - localFrom) * tHit; glm::vec3 hitClosest = closestPointOnTriangle(hitPoint, v0, v1, v2); float hitErrSq = glm::dot(hitClosest - hitPoint, hitClosest - hitPoint); - if (hitErrSq <= 0.5f * 0.5f) { + if (hitErrSq <= 0.15f * 0.15f) { float side = fromDist > 0.0f ? 1.0f : -1.0f; glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f); glm::vec3 safeWorld = glm::vec3(instance.modelMatrix * glm::vec4(safeLocal, 1.0f));