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
This commit is contained in:
Kelsi 2026-02-27 04:59:12 -08:00
parent d0e8b44866
commit 16d88f19fc
6 changed files with 120 additions and 41 deletions

View file

@ -1500,6 +1500,7 @@ private:
std::vector<AreaTriggerEntry> areaTriggers_;
std::unordered_set<uint32_t> 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<ActionBarSlot, 12> actionBar{};

View file

@ -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<std::string, TextureCacheEntry> 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<std::string> failedTextureCache_;
std::unordered_set<std::string> loggedTextureLoadFails_;
uint32_t textureBudgetRejectWarnings_ = 0;

View file

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

View file

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

View file

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

View file

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