mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Add /unstuckgy and 2GB terrain tile cache
This commit is contained in:
parent
6736ec328b
commit
132a6ea3c9
6 changed files with 214 additions and 18 deletions
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue