Add /unstuckgy and 2GB terrain tile cache

This commit is contained in:
Kelsi 2026-02-08 03:24:12 -08:00
parent 6736ec328b
commit 132a6ea3c9
6 changed files with 214 additions and 18 deletions

View file

@ -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()>;
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_;

View file

@ -14,6 +14,7 @@
#include <mutex>
#include <atomic>
#include <queue>
#include <list>
#include <vector>
#include <condition_variable>
#include <glm/glm.hpp>
@ -228,12 +229,12 @@ private:
/**
* Background thread: prepare tile data (CPU work only, no OpenGL)
*/
std::unique_ptr<PendingTile> prepareTile(int x, int y);
std::shared_ptr<PendingTile> prepareTile(int x, int y);
/**
* Main thread: upload prepared tile data to GPU
*/
void finalizeTile(std::unique_ptr<PendingTile> pending);
void finalizeTile(const std::shared_ptr<PendingTile>& pending);
/**
* Background worker thread loop
@ -282,7 +283,23 @@ private:
std::mutex queueMutex;
std::condition_variable queueCV;
std::queue<TileCoord> loadQueue;
std::queue<std::unique_ptr<PendingTile>> readyQueue;
std::queue<std::shared_ptr<PendingTile>> readyQueue;
// In-RAM tile cache (LRU) to avoid re-reading from disk
struct CachedTile {
std::shared_ptr<PendingTile> tile;
size_t bytes = 0;
std::list<TileCoord>::iterator lruIt;
};
std::unordered_map<TileCoord, CachedTile, TileCoord::Hash> tileCache_;
std::list<TileCoord> tileCacheLru_;
size_t tileCacheBytes_ = 0;
size_t tileCacheBudgetBytes_ = 2ull * 1024 * 1024 * 1024; // 2GB default
std::mutex tileCacheMutex_;
std::shared_ptr<PendingTile> getCachedTile(const TileCoord& coord);
void putCachedTile(const std::shared_ptr<PendingTile>& tile);
size_t estimatePendingTileBytes(const PendingTile& tile) const;
std::atomic<bool> workerRunning{false};
// Track tiles currently queued or being processed to avoid duplicates

View file

@ -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<float>::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<float>::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);
}
}
}
}

View file

@ -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;

View file

@ -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<PendingTile> TerrainManager::prepareTile(int x, int y) {
std::shared_ptr<PendingTile> 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<PendingTile> TerrainManager::prepareTile(int x, int y) {
return nullptr;
}
auto pending = std::make_unique<PendingTile>();
auto pending = std::make_shared<PendingTile>();
pending->coord = coord;
pending->terrain = std::move(terrain);
pending->mesh = std::move(mesh);
@ -482,7 +486,7 @@ std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
return pending;
}
void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& 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<PendingTile> 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<std::mutex> 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<PendingTile> pending;
std::shared_ptr<PendingTile> pending;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
@ -700,16 +705,16 @@ void TerrainManager::processReadyTiles() {
void TerrainManager::processAllReadyTiles() {
while (true) {
std::unique_ptr<PendingTile> pending;
std::shared_ptr<PendingTile> pending;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
@ -718,6 +723,93 @@ void TerrainManager::processAllReadyTiles() {
}
}
std::shared_ptr<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) {
std::lock_guard<std::mutex> 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<PendingTile>& tile) {
if (!tile) return;
std::lock_guard<std::mutex> 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};

View file

@ -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