diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 77bedb49..6db967d2 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -147,6 +147,7 @@ public: * Get current player movement info */ const MovementInfo& getMovementInfo() const { return movementInfo; } + uint32_t getCurrentMapId() const { return currentMapId_; } /** * Send a movement packet @@ -370,6 +371,8 @@ public: using UnstuckCallback = std::function; void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); } void unstuck(); + void setUnstuckGyCallback(UnstuckCallback cb) { unstuckGyCallback_ = std::move(cb); } + void unstuckGy(); // Creature spawn callback (online mode - triggered when creature enters view) // Parameters: guid, displayId, x, y, z (canonical), orientation @@ -833,6 +836,7 @@ private: // ---- Phase 3: Spells ---- WorldEntryCallback worldEntryCallback_; UnstuckCallback unstuckCallback_; + UnstuckCallback unstuckGyCallback_; CreatureSpawnCallback creatureSpawnCallback_; CreatureDespawnCallback creatureDespawnCallback_; CreatureMoveCallback creatureMoveCallback_; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 6c6821b4..ea336492 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -228,12 +229,12 @@ private: /** * Background thread: prepare tile data (CPU work only, no OpenGL) */ - std::unique_ptr prepareTile(int x, int y); + std::shared_ptr prepareTile(int x, int y); /** * Main thread: upload prepared tile data to GPU */ - void finalizeTile(std::unique_ptr pending); + void finalizeTile(const std::shared_ptr& pending); /** * Background worker thread loop @@ -282,7 +283,23 @@ private: std::mutex queueMutex; std::condition_variable queueCV; std::queue loadQueue; - std::queue> readyQueue; + std::queue> readyQueue; + + // In-RAM tile cache (LRU) to avoid re-reading from disk + struct CachedTile { + std::shared_ptr tile; + size_t bytes = 0; + std::list::iterator lruIt; + }; + std::unordered_map tileCache_; + std::list tileCacheLru_; + size_t tileCacheBytes_ = 0; + size_t tileCacheBudgetBytes_ = 2ull * 1024 * 1024 * 1024; // 2GB default + std::mutex tileCacheMutex_; + + std::shared_ptr getCachedTile(const TileCoord& coord); + void putCachedTile(const std::shared_ptr& tile); + size_t estimatePendingTileBytes(const PendingTile& tile) const; std::atomic workerRunning{false}; // Track tiles currently queued or being processed to avoid duplicates diff --git a/src/core/application.cpp b/src/core/application.cpp index 70e816a0..76a022df 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -586,21 +586,65 @@ void Application::setupUICallbacks() { loadOnlineWorldTerrain(mapId, x, y, z); }); - // Unstuck callback — move 5 units forward (based on facing) and re-snap to floor + // Unstuck callback — move 5 units forward gameHandler->setUnstuckCallback([this]() { if (!renderer || !renderer->getCameraController()) return; auto* cc = renderer->getCameraController(); auto* ft = cc->getFollowTargetMutable(); if (!ft) return; - // Move 5 units in the direction the player is facing float yaw = cc->getYaw(); ft->x += 5.0f * std::sin(yaw); ft->y += 5.0f * std::cos(yaw); - // Re-snap to floor at the new position cc->setDefaultSpawn(*ft, yaw, cc->getPitch()); cc->reset(); }); + // Unstuck to nearest graveyard (WorldSafeLocs.dbc) + gameHandler->setUnstuckGyCallback([this]() { + if (!renderer || !renderer->getCameraController() || !assetManager) return; + auto* cc = renderer->getCameraController(); + 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. + 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; + } + } + + if (bestDist2 == std::numeric_limits::max()) { + LOG_WARNING("No graveyard found on map ", mapId); + return; + } + + *ft = bestPos; + cc->setDefaultSpawn(bestPos, cc->getYaw(), cc->getPitch()); + cc->reset(); + }); + // Faction hostility map is built in buildFactionHostilityMap() when character enters world // Creature spawn callback (online mode) - spawn creature models @@ -2596,7 +2640,26 @@ void Application::processPendingMount() { if (!modelCached) { auto itDisplayData = displayDataMap_.find(mountDisplayId); if (itDisplayData != displayDataMap_.end()) { - const auto& dispData = itDisplayData->second; + CreatureDisplayData dispData = itDisplayData->second; + // If this displayId has no skins, try to find another displayId for the same model with skins. + if (dispData.skin1.empty() && dispData.skin2.empty() && dispData.skin3.empty()) { + uint32_t modelId = dispData.modelId; + int bestScore = -1; + for (const auto& [dispId, data] : displayDataMap_) { + if (data.modelId != modelId) continue; + int score = 0; + if (!data.skin1.empty()) score += 3; + if (!data.skin2.empty()) score += 2; + if (!data.skin3.empty()) score += 1; + if (score > bestScore) { + bestScore = score; + dispData = data; + } + } + LOG_INFO("Mount skin fallback for displayId=", mountDisplayId, + " modelId=", modelId, " skin1='", dispData.skin1, + "' skin2='", dispData.skin2, "' skin3='", dispData.skin3, "'"); + } const auto* md = charRenderer->getModelData(modelId); if (md) { std::string modelDir; @@ -2631,6 +2694,13 @@ void Application::processPendingMount() { charRenderer->setModelTexture(modelId, 0, skinTex); LOG_INFO("Forced mount skin1 texture on slot 0: ", texPath); } + } else if (replaced == 0 && !md->textures.empty() && !md->textures[0].filename.empty()) { + // Last-resort: use the model's first texture filename if it exists. + GLuint texId = charRenderer->loadTexture(md->textures[0].filename); + if (texId != 0) { + charRenderer->setModelTexture(modelId, 0, texId); + LOG_INFO("Forced mount model texture on slot 0: ", md->textures[0].filename); + } } } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5cb93810..2afa7acb 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4438,6 +4438,13 @@ void GameHandler::unstuck() { } } +void GameHandler::unstuckGy() { + if (unstuckGyCallback_) { + unstuckGyCallback_(); + addSystemChatMessage("Unstuck: moved to nearest graveyard."); + } +} + void GameHandler::handleLootResponse(network::Packet& packet) { if (!LootResponseParser::parse(packet, currentLoot)) return; lootWindowOpen = true; diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 2eac5483..7405a002 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -190,7 +190,7 @@ bool TerrainManager::loadTile(int x, int y) { return false; } - finalizeTile(std::move(pending)); + finalizeTile(pending); return true; } @@ -215,8 +215,12 @@ bool TerrainManager::enqueueTile(int x, int y) { return true; } -std::unique_ptr TerrainManager::prepareTile(int x, int y) { +std::shared_ptr TerrainManager::prepareTile(int x, int y) { TileCoord coord = {x, y}; + if (auto cached = getCachedTile(coord)) { + LOG_DEBUG("Using cached tile [", x, ",", y, "]"); + return cached; + } LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)"); @@ -247,7 +251,7 @@ std::unique_ptr TerrainManager::prepareTile(int x, int y) { return nullptr; } - auto pending = std::make_unique(); + auto pending = std::make_shared(); pending->coord = coord; pending->terrain = std::move(terrain); pending->mesh = std::move(mesh); @@ -482,7 +486,7 @@ std::unique_ptr TerrainManager::prepareTile(int x, int y) { return pending; } -void TerrainManager::finalizeTile(std::unique_ptr pending) { +void TerrainManager::finalizeTile(const std::shared_ptr& pending) { int x = pending->coord.x; int y = pending->coord.y; TileCoord coord = pending->coord; @@ -623,6 +627,7 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY); loadedTiles[coord] = std::move(tile); + putCachedTile(pending); LOG_DEBUG(" Finalized tile [", x, ",", y, "]"); } @@ -656,7 +661,7 @@ void TerrainManager::workerLoop() { std::lock_guard lock(queueMutex); if (pending) { - readyQueue.push(std::move(pending)); + readyQueue.push(pending); } else { // Mark as failed so we don't re-enqueue // We'll set failedTiles on the main thread in processReadyTiles @@ -675,20 +680,20 @@ void TerrainManager::processReadyTiles() { const int maxPerFrame = 1; while (processed < maxPerFrame) { - std::unique_ptr pending; + std::shared_ptr pending; { std::lock_guard lock(queueMutex); if (readyQueue.empty()) { break; } - pending = std::move(readyQueue.front()); + pending = readyQueue.front(); readyQueue.pop(); } if (pending) { TileCoord coord = pending->coord; - finalizeTile(std::move(pending)); + finalizeTile(pending); { std::lock_guard lock(queueMutex); pendingTiles.erase(coord); @@ -700,16 +705,16 @@ void TerrainManager::processReadyTiles() { void TerrainManager::processAllReadyTiles() { while (true) { - std::unique_ptr pending; + std::shared_ptr pending; { std::lock_guard lock(queueMutex); if (readyQueue.empty()) break; - pending = std::move(readyQueue.front()); + pending = readyQueue.front(); readyQueue.pop(); } if (pending) { TileCoord coord = pending->coord; - finalizeTile(std::move(pending)); + finalizeTile(pending); { std::lock_guard lock(queueMutex); pendingTiles.erase(coord); @@ -718,6 +723,93 @@ void TerrainManager::processAllReadyTiles() { } } +std::shared_ptr TerrainManager::getCachedTile(const TileCoord& coord) { + std::lock_guard lock(tileCacheMutex_); + auto it = tileCache_.find(coord); + if (it == tileCache_.end()) return nullptr; + tileCacheLru_.erase(it->second.lruIt); + tileCacheLru_.push_front(coord); + it->second.lruIt = tileCacheLru_.begin(); + return it->second.tile; +} + +void TerrainManager::putCachedTile(const std::shared_ptr& tile) { + if (!tile) return; + std::lock_guard lock(tileCacheMutex_); + TileCoord coord = tile->coord; + + auto it = tileCache_.find(coord); + if (it != tileCache_.end()) { + tileCacheLru_.erase(it->second.lruIt); + tileCacheBytes_ -= it->second.bytes; + tileCache_.erase(it); + } + + size_t bytes = estimatePendingTileBytes(*tile); + tileCacheLru_.push_front(coord); + tileCache_[coord] = CachedTile{tile, bytes, tileCacheLru_.begin()}; + tileCacheBytes_ += bytes; + + // Evict least-recently used tiles until under budget + while (tileCacheBytes_ > tileCacheBudgetBytes_ && !tileCacheLru_.empty()) { + TileCoord evictCoord = tileCacheLru_.back(); + auto eit = tileCache_.find(evictCoord); + if (eit != tileCache_.end()) { + tileCacheBytes_ -= eit->second.bytes; + tileCache_.erase(eit); + } + tileCacheLru_.pop_back(); + } +} + +size_t TerrainManager::estimatePendingTileBytes(const PendingTile& tile) const { + size_t bytes = 0; + bytes += sizeof(PendingTile); + bytes += tile.terrain.textures.size() * 64; + bytes += tile.terrain.doodadNames.size() * 64; + bytes += tile.terrain.wmoNames.size() * 64; + bytes += tile.terrain.doodadPlacements.size() * sizeof(pipeline::ADTTerrain::DoodadPlacement); + bytes += tile.terrain.wmoPlacements.size() * sizeof(pipeline::ADTTerrain::WMOPlacement); + + for (const auto& chunk : tile.terrain.chunks) { + bytes += sizeof(chunk); + bytes += chunk.layers.size() * sizeof(pipeline::TextureLayer); + bytes += chunk.alphaMap.size(); + } + + for (const auto& cm : tile.mesh.chunks) { + bytes += cm.vertices.size() * sizeof(pipeline::TerrainVertex); + bytes += cm.indices.size() * sizeof(pipeline::TerrainIndex); + for (const auto& layer : cm.layers) { + bytes += layer.alphaData.size(); + } + } + + for (const auto& ready : tile.m2Models) { + bytes += ready.model.vertices.size() * sizeof(pipeline::M2Vertex); + bytes += ready.model.indices.size() * sizeof(uint16_t); + bytes += ready.model.textures.size() * sizeof(pipeline::M2Texture); + } + bytes += tile.m2Placements.size() * sizeof(PendingTile::M2Placement); + + for (const auto& ready : tile.wmoModels) { + for (const auto& group : ready.model.groups) { + bytes += group.vertices.size() * sizeof(pipeline::WMOVertex); + bytes += group.indices.size() * sizeof(uint16_t); + bytes += group.batches.size() * sizeof(pipeline::WMOBatch); + bytes += group.portalVertices.size() * sizeof(glm::vec3); + bytes += group.portals.size() * sizeof(pipeline::WMOPortal); + bytes += group.bspNodes.size(); + } + } + bytes += tile.wmoDoodads.size() * sizeof(PendingTile::WMODoodadReady); + + for (const auto& [_, img] : tile.preloadedTextures) { + bytes += img.data.size(); + } + return bytes; +} + void TerrainManager::unloadTile(int x, int y) { TileCoord coord = {x, y}; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index c2d5e652..215ccb9d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1728,6 +1728,12 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { chatInputBuffer[0] = '\0'; return; } + // /unstuckgy command — move to nearest graveyard + if (cmdLower == "unstuckgy") { + gameHandler.unstuckGy(); + chatInputBuffer[0] = '\0'; + return; + } // Chat channel slash commands // If used without a message (e.g. just "/s"), switch the chat type dropdown