From f752a4f517dce39e5ec756d69095a6b3971fe78b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Feb 2026 18:25:04 -0800 Subject: [PATCH] Fix NPC visibility and stabilize world transport/taxi updates --- include/audio/music_manager.hpp | 6 + include/core/application.hpp | 8 +- include/game/game_handler.hpp | 13 +- include/game/zone_manager.hpp | 1 + include/rendering/renderer.hpp | 3 + include/rendering/world_map.hpp | 4 + src/audio/music_manager.cpp | 23 ++- src/core/application.cpp | 207 ++++++++++++++++----------- src/game/game_handler.cpp | 96 +++++++++---- src/game/zone_manager.cpp | 16 +++ src/pipeline/asset_manager.cpp | 8 +- src/rendering/character_renderer.cpp | 27 +++- src/rendering/m2_renderer.cpp | 23 +-- src/rendering/renderer.cpp | 79 ++++++++-- src/rendering/world_map.cpp | 106 +++++++++++++- src/ui/game_screen.cpp | 5 +- 16 files changed, 452 insertions(+), 173 deletions(-) diff --git a/include/audio/music_manager.hpp b/include/audio/music_manager.hpp index 0d260e36..ba56a5d5 100644 --- a/include/audio/music_manager.hpp +++ b/include/audio/music_manager.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include namespace wowee { namespace pipeline { class AssetManager; } @@ -23,6 +26,7 @@ public: void setVolume(int volume); int getVolume() const { return volumePercent; } void setUnderwaterMode(bool underwater); + void preloadMusic(const std::string& mpqPath); bool isPlaying() const { return playing; } bool isInitialized() const { return assetManager != nullptr; } @@ -41,6 +45,8 @@ private: std::string pendingTrack; float fadeTimer = 0.0f; float fadeDuration = 0.0f; + + std::unordered_map> musicDataCache_; }; } // namespace audio diff --git a/include/core/application.hpp b/include/core/application.hpp index 4c8de8fd..c49bbf30 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace wowee { @@ -192,7 +193,12 @@ private: float x, y, z, orientation; }; std::vector pendingCreatureSpawns_; - static constexpr int MAX_SPAWNS_PER_FRAME = 2; + static constexpr int MAX_SPAWNS_PER_FRAME = 24; + static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300; + std::unordered_set pendingCreatureSpawnGuids_; + std::unordered_map creatureSpawnRetryCounts_; + std::unordered_set nonRenderableCreatureDisplayIds_; + std::unordered_set creaturePermanentFailureGuids_; void processCreatureSpawnQueue(); struct PendingGameObjectSpawn { diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 57bfbe7f..5b71d08c 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -427,6 +427,8 @@ public: uint32_t getPlayerXp() const { return playerXp_; } uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; } uint32_t getPlayerLevel() const { return serverPlayerLevel_; } + const std::vector& getPlayerExploredZoneMasks() const { return playerExploredZones_; } + bool hasPlayerExploredZoneMasks() const { return hasPlayerExploredZones_; } static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel); // Server time (for deterministic moon phases, etc.) @@ -1085,11 +1087,8 @@ private: float taxiLandingCooldown_ = 0.0f; // Prevent re-entering taxi right after landing size_t taxiClientIndex_ = 0; std::vector taxiClientPath_; - float taxiClientSpeed_ = 18.0f; // Reduced from 32 to prevent loading hitches + float taxiClientSpeed_ = 32.0f; float taxiClientSegmentProgress_ = 0.0f; - bool taxiMountingDelay_ = false; // Delay before flight starts (terrain precache time) - float taxiMountingTimer_ = 0.0f; - std::vector taxiPendingPath_; // Path nodes waiting for mounting delay bool taxiRecoverPending_ = false; uint32_t taxiRecoverMapId_ = 0; glm::vec3 taxiRecoverPos_{0.0f}; @@ -1144,9 +1143,15 @@ private: std::unordered_map spellToSkillLine_; // spellID -> skillLineID bool skillLineDbcLoaded_ = false; bool skillLineAbilityLoaded_ = false; + static constexpr uint16_t PLAYER_EXPLORED_ZONES_START = 1041; // 3.3.5a UpdateFields + static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128; + std::vector playerExploredZones_ = + std::vector(PLAYER_EXPLORED_ZONES_COUNT, 0u); + bool hasPlayerExploredZones_ = false; void loadSkillLineDbc(); void loadSkillLineAbilityDbc(); void extractSkillFields(const std::map& fields); + void extractExploredZoneFields(const std::map& fields); NpcDeathCallback npcDeathCallback_; NpcAggroCallback npcAggroCallback_; diff --git a/include/game/zone_manager.hpp b/include/game/zone_manager.hpp index 6b16a0ef..06f43baf 100644 --- a/include/game/zone_manager.hpp +++ b/include/game/zone_manager.hpp @@ -21,6 +21,7 @@ public: uint32_t getZoneId(int tileX, int tileY) const; const ZoneInfo* getZoneInfo(uint32_t zoneId) const; std::string getRandomMusic(uint32_t zoneId) const; + std::vector getAllMusicPaths() const; private: // tile key = tileX * 100 + tileY diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index a82024ca..b39c0c4f 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -323,6 +323,9 @@ private: float mountHeightOffset_ = 0.0f; float mountPitch_ = 0.0f; // Up/down tilt (radians) float mountRoll_ = 0.0f; // Left/right banking (radians) + int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable + glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f); + bool mountSeatSmoothingInit_ = false; float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean) float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation() MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up) diff --git a/include/rendering/world_map.hpp b/include/rendering/world_map.hpp index b2b7c420..128afc00 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -20,6 +20,7 @@ struct WorldMapZone { float locLeft = 0, locRight = 0, locTop = 0, locBottom = 0; uint32_t displayMapID = 0; uint32_t parentWorldMapID = 0; + uint32_t exploreFlag = 0; // Per-zone cached textures GLuint tileTextures[12] = {}; @@ -34,6 +35,7 @@ public: void initialize(pipeline::AssetManager* assetManager); void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); void setMapName(const std::string& name); + void setServerExplorationMask(const std::vector& masks, bool hasData); bool isOpen() const { return open; } void close() { open = false; } @@ -87,6 +89,8 @@ private: GLuint tileQuadVBO = 0; // Exploration / fog of war + std::vector serverExplorationMask; + bool hasServerExplorationMask = false; std::unordered_set exploredZones; // zone indices the player has visited }; diff --git a/src/audio/music_manager.cpp b/src/audio/music_manager.cpp index eab07702..57003a28 100644 --- a/src/audio/music_manager.cpp +++ b/src/audio/music_manager.cpp @@ -24,6 +24,17 @@ void MusicManager::shutdown() { AudioEngine::instance().stopMusic(); playing = false; currentTrack.clear(); + musicDataCache_.clear(); +} + +void MusicManager::preloadMusic(const std::string& mpqPath) { + if (!assetManager || mpqPath.empty()) return; + if (musicDataCache_.find(mpqPath) != musicDataCache_.end()) return; + + auto data = assetManager->readFile(mpqPath); + if (!data.empty()) { + musicDataCache_[mpqPath] = std::move(data); + } } void MusicManager::playMusic(const std::string& mpqPath, bool loop) { @@ -36,9 +47,13 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) { return; } - // Read music file from MPQ - auto data = assetManager->readFile(mpqPath); - if (data.empty()) { + // Read music file from cache or MPQ + auto cacheIt = musicDataCache_.find(mpqPath); + if (cacheIt == musicDataCache_.end()) { + preloadMusic(mpqPath); + cacheIt = musicDataCache_.find(mpqPath); + } + if (cacheIt == musicDataCache_.end() || cacheIt->second.empty()) { LOG_WARNING("Music: Could not read: ", mpqPath); return; } @@ -48,7 +63,7 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) { // Play with AudioEngine (non-blocking, streams from memory) float volume = volumePercent / 100.0f; - if (AudioEngine::instance().playMusic(data, volume, loop)) { + if (AudioEngine::instance().playMusic(cacheIt->second, volume, loop)) { playing = true; currentTrack = mpqPath; currentTrackIsFile = false; diff --git a/src/core/application.cpp b/src/core/application.cpp index 92bc1b20..499f1c5e 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -467,23 +467,7 @@ void Application::update(float deltaTime) { } if (renderer && renderer->getTerrainManager()) { renderer->getTerrainManager()->setStreamingEnabled(true); - // With 8GB tile cache and precaching, minimize streaming during taxi - if (onTaxi) { - renderer->getTerrainManager()->setUpdateInterval(2.0f); // Very infrequent updates - already precached - renderer->getTerrainManager()->setLoadRadius(2); // 5x5 grid for taxi (each tile ~533 yards) - } else { - // Ramp streaming back in after taxi to avoid end-of-flight hitches. - if (lastTaxiFlight_) { - taxiStreamCooldown_ = 2.5f; - renderer->getTerrainManager()->setLoadRadius(2); // Back to 5x5 - } - if (taxiStreamCooldown_ > 0.0f) { - taxiStreamCooldown_ -= deltaTime; - renderer->getTerrainManager()->setUpdateInterval(1.0f); - } else { - renderer->getTerrainManager()->setUpdateInterval(0.1f); - } - } + renderer->getTerrainManager()->setUpdateInterval(0.1f); } lastTaxiFlight_ = onTaxi; @@ -760,8 +744,12 @@ void Application::setupUICallbacks() { // Creature spawn callback (online mode) - spawn creature models gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { - // Queue spawns to avoid hanging when many creatures appear at once + // Queue spawns to avoid hanging when many creatures appear at once. + // Deduplicate so repeated updates don't flood pending queue. + if (creatureInstances_.count(guid)) return; + if (pendingCreatureSpawnGuids_.count(guid)) return; pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation}); + pendingCreatureSpawnGuids_.insert(guid); }); // Creature despawn callback (online mode) - remove creature models @@ -2298,10 +2286,17 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Skip if already spawned if (creatureInstances_.count(guid)) return; + if (nonRenderableCreatureDisplayIds_.count(displayId)) { + creaturePermanentFailureGuids_.insert(guid); + return; + } + // Get model path from displayId std::string m2Path = getModelPathForDisplayId(displayId); if (m2Path.empty()) { LOG_WARNING("No model path for displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")"); + nonRenderableCreatureDisplayIds_.insert(displayId); + creaturePermanentFailureGuids_.insert(guid); return; } @@ -2321,12 +2316,15 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x auto m2Data = assetManager->readFile(m2Path); if (m2Data.empty()) { LOG_WARNING("Failed to read creature M2: ", m2Path); + creaturePermanentFailureGuids_.insert(guid); return; } pipeline::M2Model model = pipeline::M2Loader::load(m2Data); if (model.vertices.empty()) { LOG_WARNING("Failed to parse creature M2: ", m2Path); + nonRenderableCreatureDisplayIds_.insert(displayId); + creaturePermanentFailureGuids_.insert(guid); return; } @@ -2353,6 +2351,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (!charRenderer->loadModel(model, modelId)) { LOG_WARNING("Failed to load creature model: ", m2Path); + creaturePermanentFailureGuids_.insert(guid); return; } @@ -2497,7 +2496,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Smart filtering for bad spawn data: // - If over ocean AND at continental height (Z > 50): bad data, skip // - If over ocean AND near sea level (Z <= 50): water creature, allow - // - If over land: snap to terrain height + // - If over land: preserve server Z for elevated platforms/roofs and only + // correct clearly underground spawns. if (renderer->getTerrainManager()) { auto terrainH = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y); if (!terrainH) { @@ -2508,8 +2508,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } // Low altitude = probably legitimate water creature, allow spawn at original Z } else { - // Valid terrain found - snap to terrain height - renderPos.z = *terrainH + 0.1f; + // Keep authentic server Z for NPCs on raised geometry (e.g. flight masters). + // Only lift up if spawn is clearly below terrain. + if (renderPos.z < (*terrainH - 2.0f)) { + renderPos.z = *terrainH + 0.1f; + } } } @@ -2763,6 +2766,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Track instance creatureInstances_[guid] = instanceId; creatureModelIds_[guid] = modelId; + creaturePermanentFailureGuids_.erase(guid); LOG_INFO("Spawned creature: guid=0x", std::hex, guid, std::dec, " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); @@ -3051,13 +3055,43 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t void Application::processCreatureSpawnQueue() { if (pendingCreatureSpawns_.empty()) return; + if (!creatureLookupsBuilt_) { + buildCreatureDisplayLookups(); + if (!creatureLookupsBuilt_) return; + } - int spawned = 0; - while (!pendingCreatureSpawns_.empty() && spawned < MAX_SPAWNS_PER_FRAME) { - auto& s = pendingCreatureSpawns_.front(); + int processed = 0; + while (!pendingCreatureSpawns_.empty() && processed < MAX_SPAWNS_PER_FRAME) { + PendingCreatureSpawn s = pendingCreatureSpawns_.front(); spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation); pendingCreatureSpawns_.erase(pendingCreatureSpawns_.begin()); - spawned++; + pendingCreatureSpawnGuids_.erase(s.guid); + + // If spawn still failed, retry for a limited number of frames. + if (!creatureInstances_.count(s.guid)) { + if (creaturePermanentFailureGuids_.erase(s.guid) > 0) { + creatureSpawnRetryCounts_.erase(s.guid); + processed++; + continue; + } + uint16_t retries = 0; + auto it = creatureSpawnRetryCounts_.find(s.guid); + if (it != creatureSpawnRetryCounts_.end()) { + retries = it->second; + } + if (retries < MAX_CREATURE_SPAWN_RETRIES) { + creatureSpawnRetryCounts_[s.guid] = static_cast(retries + 1); + pendingCreatureSpawns_.push_back(s); + pendingCreatureSpawnGuids_.insert(s.guid); + } else { + creatureSpawnRetryCounts_.erase(s.guid); + LOG_WARNING("Dropping creature spawn after retries: guid=0x", std::hex, s.guid, std::dec, + " displayId=", s.displayId); + } + } else { + creatureSpawnRetryCounts_.erase(s.guid); + } + processed++; } } @@ -3090,11 +3124,9 @@ void Application::processPendingMount() { // Check model cache uint32_t modelId = 0; - bool modelCached = false; auto cacheIt = displayIdModelCache_.find(mountDisplayId); if (cacheIt != displayIdModelCache_.end()) { modelId = cacheIt->second; - modelCached = true; } else { modelId = nextCreatureModelId_++; @@ -3142,73 +3174,72 @@ void Application::processPendingMount() { displayIdModelCache_[mountDisplayId] = modelId; } - // Apply creature skin textures from CreatureDisplayInfo.dbc - if (!modelCached) { - auto itDisplayData = displayDataMap_.find(mountDisplayId); - if (itDisplayData != displayDataMap_.end()) { - 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; - } + // Apply creature skin textures from CreatureDisplayInfo.dbc. + // Re-apply even for cached models so transient failures can self-heal. + auto itDisplayData = displayDataMap_.find(mountDisplayId); + if (itDisplayData != displayDataMap_.end()) { + 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; - size_t lastSlash = m2Path.find_last_of("\\/"); - if (lastSlash != std::string::npos) { - modelDir = m2Path.substr(0, lastSlash + 1); + 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; + size_t lastSlash = m2Path.find_last_of("\\/"); + if (lastSlash != std::string::npos) { + modelDir = m2Path.substr(0, lastSlash + 1); + } + int replaced = 0; + for (size_t ti = 0; ti < md->textures.size(); ti++) { + const auto& tex = md->textures[ti]; + std::string texPath; + if (tex.type == 11 && !dispData.skin1.empty()) { + texPath = modelDir + dispData.skin1 + ".blp"; + } else if (tex.type == 12 && !dispData.skin2.empty()) { + texPath = modelDir + dispData.skin2 + ".blp"; + } else if (tex.type == 13 && !dispData.skin3.empty()) { + texPath = modelDir + dispData.skin3 + ".blp"; } - int replaced = 0; - for (size_t ti = 0; ti < md->textures.size(); ti++) { - const auto& tex = md->textures[ti]; - std::string texPath; - if (tex.type == 11 && !dispData.skin1.empty()) { - texPath = modelDir + dispData.skin1 + ".blp"; - } else if (tex.type == 12 && !dispData.skin2.empty()) { - texPath = modelDir + dispData.skin2 + ".blp"; - } else if (tex.type == 13 && !dispData.skin3.empty()) { - texPath = modelDir + dispData.skin3 + ".blp"; - } - if (!texPath.empty()) { - GLuint skinTex = charRenderer->loadTexture(texPath); - if (skinTex != 0) { - charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); - replaced++; - } - } - } - // Some mounts (gryphon/wyvern) use empty model textures; force skin1 onto slot 0. - if (replaced == 0 && !dispData.skin1.empty() && !md->textures.empty()) { - std::string texPath = modelDir + dispData.skin1 + ".blp"; + if (!texPath.empty()) { GLuint skinTex = charRenderer->loadTexture(texPath); if (skinTex != 0) { - 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); + charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); + replaced++; } } } + // Some mounts (gryphon/wyvern) use empty model textures; force skin1 onto slot 0. + if (replaced == 0 && !dispData.skin1.empty() && !md->textures.empty()) { + std::string texPath = modelDir + dispData.skin1 + ".blp"; + GLuint skinTex = charRenderer->loadTexture(texPath); + if (skinTex != 0) { + 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); + } + } } } @@ -3255,6 +3286,10 @@ void Application::processPendingMount() { } void Application::despawnOnlineCreature(uint64_t guid) { + pendingCreatureSpawnGuids_.erase(guid); + creatureSpawnRetryCounts_.erase(guid); + creaturePermanentFailureGuids_.erase(guid); + auto it = creatureInstances_.find(guid); if (it == creatureInstances_.end()) return; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4e79d0de..a9f78019 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -288,24 +288,6 @@ void GameHandler::update(float deltaTime) { } } - // Mounting delay for taxi (terrain + M2 model precache time) - if (taxiMountingDelay_) { - taxiMountingTimer_ += deltaTime; - // 5 second delay for terrain and M2 models to load and upload to VRAM - if (taxiMountingTimer_ >= 5.0f) { - taxiMountingDelay_ = false; - taxiMountingTimer_ = 0.0f; - // Upload all precached tiles to GPU before flight starts - if (taxiFlightStartCallback_) { - taxiFlightStartCallback_(); - } - if (!taxiPendingPath_.empty()) { - startClientTaxiPath(taxiPendingPath_); - taxiPendingPath_.clear(); - } - } - } - auto taxiEnd = std::chrono::high_resolution_clock::now(); taxiTime += std::chrono::duration(taxiEnd - taxiStart).count(); @@ -1524,6 +1506,8 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { playerXp_ = 0; playerNextLevelXp_ = 0; serverPlayerLevel_ = 1; + std::fill(playerExploredZones_.begin(), playerExploredZones_.end(), 0u); + hasPlayerExploredZones_ = false; playerSkills_.clear(); questLog_.clear(); npcQuestStatus_.clear(); @@ -2235,6 +2219,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); extractSkillFields(lastPlayerFields_); + extractExploredZoneFields(lastPlayerFields_); } break; } @@ -2251,6 +2236,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; + uint32_t oldDisplayId = unit->getDisplayId(); + bool displayIdChanged = false; for (const auto& [key, val] : block.fields) { switch (key) { case 24: { @@ -2316,7 +2303,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { unit->setFactionTemplate(val); unit->setHostile(isHostileFaction(val)); break; - case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID + case 67: + if (val != unit->getDisplayId()) { + unit->setDisplayId(val); + displayIdChanged = true; + } + break; // UNIT_FIELD_DISPLAYID case 69: // UNIT_FIELD_MOUNTDISPLAYID if (block.guid == playerGuid) { uint32_t old = currentMountDisplayId_; @@ -2333,6 +2325,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { default: break; } } + + // Some units are created without displayId and get it later via VALUES. + if (entity->getType() == ObjectType::UNIT && + displayIdChanged && + unit->getDisplayId() != 0 && + unit->getDisplayId() != oldDisplayId) { + if (creatureSpawnCallback_) { + creatureSpawnCallback_(block.guid, unit->getDisplayId(), + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + } + if ((unit->getNpcFlags() & 0x02) && socket) { + network::Packet qsPkt(static_cast(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + qsPkt.writeUInt64(block.guid); + socket->send(qsPkt); + } + } } // Update XP / inventory slot / skill fields for player entity if (block.guid == playerGuid) { @@ -2387,6 +2395,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); extractSkillFields(lastPlayerFields_); + extractExploredZoneFields(lastPlayerFields_); } // Update item stack count for online items @@ -6125,10 +6134,12 @@ void GameHandler::startClientTaxiPath(const std::vector& pathNodes) { glm::vec3 start = taxiClientPath_[0]; glm::vec3 end = taxiClientPath_[1]; glm::vec3 dir = end - start; + float dirLen = glm::length(dir); + if (dirLen < 0.001f) return; float initialOrientation = std::atan2(dir.y, dir.x) - 1.57079632679f; // Calculate initial pitch from altitude change - glm::vec3 dirNorm = glm::normalize(dir); + glm::vec3 dirNorm = dir / dirLen; float initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f)); float initialRoll = 0.0f; // No initial banking @@ -6218,12 +6229,21 @@ void GameHandler::updateClientTaxi(float deltaTime) { 2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t + 3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2 ); + float tangentLen = glm::length(tangent); + if (tangentLen < 0.0001f) { + tangent = dir; + tangentLen = glm::length(tangent); + if (tangentLen < 0.0001f) { + tangent = glm::vec3(std::cos(movementInfo.orientation), std::sin(movementInfo.orientation), 0.0f); + tangentLen = glm::length(tangent); + } + } // Calculate yaw from horizontal direction float targetOrientation = std::atan2(tangent.y, tangent.x) - 1.57079632679f; // Calculate pitch from vertical component (altitude change) - glm::vec3 tangentNorm = glm::normalize(tangent); + glm::vec3 tangentNorm = tangent / std::max(tangentLen, 0.0001f); float pitch = std::asin(std::clamp(tangentNorm.z, -1.0f, 1.0f)); // Calculate roll (banking) from rate of yaw change @@ -6419,12 +6439,7 @@ void GameHandler::activateTaxi(uint32_t destNodeId) { applyTaxiMountForCurrentNode(); } - // Start mounting delay (gives terrain precache time to load) - taxiMountingDelay_ = true; - taxiMountingTimer_ = 0.0f; - taxiPendingPath_ = path; - - // Trigger terrain precache immediately (uses mounting delay time to load) + // Trigger terrain precache immediately (non-blocking). if (taxiPrecacheCallback_) { std::vector previewPath; // Build full spline path using TaxiPathNode waypoints @@ -6455,7 +6470,13 @@ void GameHandler::activateTaxi(uint32_t destNodeId) { } } - addSystemChatMessage("Mounting for flight..."); + // Flight starts immediately; upload callback stays opportunistic/non-blocking. + if (taxiFlightStartCallback_) { + taxiFlightStartCallback_(); + } + startClientTaxiPath(path); + + addSystemChatMessage("Flight started."); // Save recovery target in case of disconnect during taxi. auto destIt = taxiNodes_.find(destNodeId); @@ -6804,6 +6825,25 @@ void GameHandler::extractSkillFields(const std::map& fields) playerSkills_ = std::move(newSkills); } +void GameHandler::extractExploredZoneFields(const std::map& fields) { + if (playerExploredZones_.size() != PLAYER_EXPLORED_ZONES_COUNT) { + playerExploredZones_.assign(PLAYER_EXPLORED_ZONES_COUNT, 0u); + } + + bool foundAny = false; + for (size_t i = 0; i < PLAYER_EXPLORED_ZONES_COUNT; i++) { + const uint16_t fieldIdx = static_cast(PLAYER_EXPLORED_ZONES_START + i); + auto it = fields.find(fieldIdx); + if (it == fields.end()) continue; + playerExploredZones_[i] = it->second; + foundAny = true; + } + + if (foundAny) { + hasPlayerExploredZones_ = true; + } +} + std::string GameHandler::getCharacterConfigDir() { std::string dir; #ifdef _WIN32 diff --git a/src/game/zone_manager.cpp b/src/game/zone_manager.cpp index 208bc1db..48affebd 100644 --- a/src/game/zone_manager.cpp +++ b/src/game/zone_manager.cpp @@ -2,6 +2,7 @@ #include "core/logger.hpp" #include #include +#include namespace wowee { namespace game { @@ -112,5 +113,20 @@ std::string ZoneManager::getRandomMusic(uint32_t zoneId) const { return paths[std::rand() % paths.size()]; } +std::vector ZoneManager::getAllMusicPaths() const { + std::vector out; + std::unordered_set seen; + for (const auto& [zoneId, zone] : zones) { + (void)zoneId; + for (const auto& path : zone.musicPaths) { + if (path.empty()) continue; + if (seen.insert(path).second) { + out.push_back(path); + } + } + } + return out; +} + } // namespace game } // namespace wowee diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 22e0497c..5a4f81d2 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -68,12 +68,8 @@ BLPImage AssetManager::loadTexture(const std::string& path) { LOG_DEBUG("Loading texture: ", 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); - } + // Route through readFile() so decompressed MPQ bytes participate in file cache. + std::vector blpData = readFile(normalizedPath); if (blpData.empty()) { LOG_WARNING("Texture not found: ", normalizedPath); return BLPImage(); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index f8d93651..c1e65114 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -323,7 +323,8 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) { auto blpImage = assetManager->loadTexture(path); if (!blpImage.isValid()) { core::Logger::getInstance().warning("Failed to load texture: ", path); - textureCache[path] = whiteTexture; + // Do not cache failures as white. Some asset reads can fail transiently and + // we want later retries (e.g., mount skins loaded shortly after model spawn). return whiteTexture; } @@ -1257,6 +1258,26 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons glBindVertexArray(gpuModel.vao); if (!gpuModel.data.batches.empty()) { + bool applyGeosetFilter = !instance.activeGeosets.empty(); + if (applyGeosetFilter) { + bool hasRenderableGeoset = false; + for (const auto& batch : gpuModel.data.batches) { + if (instance.activeGeosets.find(batch.submeshId) != instance.activeGeosets.end()) { + hasRenderableGeoset = true; + break; + } + } + if (!hasRenderableGeoset) { + static std::unordered_set loggedGeosetFallback; + if (loggedGeosetFallback.insert(instance.id).second) { + LOG_WARNING("Geoset filter matched no batches for instance ", + instance.id, " (model ", instance.modelId, + "); rendering all batches as fallback"); + } + applyGeosetFilter = false; + } + } + // One-time debug dump of rendered batches per model static std::unordered_set dumpedModels; if (dumpedModels.find(instance.modelId) == dumpedModels.end()) { @@ -1264,7 +1285,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons int bIdx = 0; int rendered = 0, skipped = 0; for (const auto& b : gpuModel.data.batches) { - bool filtered = !instance.activeGeosets.empty() && + bool filtered = applyGeosetFilter && (b.submeshId / 100 != 0) && instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end(); @@ -1304,7 +1325,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons // For character models, group 0 (body/scalp) is also filtered so that only // the correct scalp mesh renders (not all overlapping variants). for (const auto& batch : gpuModel.data.batches) { - if (!instance.activeGeosets.empty()) { + if (applyGeosetFilter) { if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { continue; } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 2184b9e6..d6c88427 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1661,14 +1661,13 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: shader->setUniform("uProjection", projection); shader->setUniform("uLightDir", lightDir); shader->setUniform("uLightColor", lightColor); - shader->setUniform("uSpecularIntensity", onTaxi_ ? 0.0f : 0.5f); // Disable specular during taxi for performance + shader->setUniform("uSpecularIntensity", 0.5f); shader->setUniform("uAmbientColor", ambientColor); shader->setUniform("uViewPos", camera.getPosition()); shader->setUniform("uFogColor", fogColor); shader->setUniform("uFogStart", fogStart); shader->setUniform("uFogEnd", fogEnd); - // Disable shadows during taxi for better performance - bool useShadows = shadowEnabled && !onTaxi_; + bool useShadows = shadowEnabled; shader->setUniform("uShadowEnabled", useShadows ? 1 : 0); shader->setUniform("uShadowStrength", 0.65f); if (useShadows) { @@ -1681,7 +1680,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; // Adaptive render distance: balanced for performance without excessive pop-in - const float maxRenderDistance = onTaxi_ ? 700.0f : (instances.size() > 2000) ? 350.0f : 1000.0f; + const float maxRenderDistance = (instances.size() > 2000) ? 350.0f : 1000.0f; const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const float fadeStartFraction = 0.75f; const glm::vec3 camPos = camera.getPosition(); @@ -1787,22 +1786,6 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: const M2ModelGPU& model = *currentModel; - // Relaxed culling during taxi (VRAM caching eliminates loading hitches) - if (onTaxi_) { - // Skip tiny props (barrels, crates, small debris) - if (model.boundRadius < 2.0f) { - continue; - } - // Skip small ground foliage (bushes, flowers) but keep trees - if (model.collisionNoBlock && model.boundRadius < 5.0f) { - continue; - } - // Skip deep underwater objects (opaque water hides them anyway) - if (instance.position.z < -10.0f) { - continue; - } - } - // Distance-based fade alpha for smooth pop-in (squared-distance, no sqrt) float fadeAlpha = 1.0f; float fadeFrac = model.disableAnimation ? 0.55f : fadeStartFraction; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 6b36f94b..6bc18f67 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -557,6 +557,9 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) { mountInstanceId_ = mountInstId; mountHeightOffset_ = heightOffset; + mountSeatAttachmentId_ = -1; + smoothedMountSeatPos_ = characterPosition; + mountSeatSmoothingInit_ = false; mountAction_ = MountAction::None; // Clear mount action state mountActionPhase_ = 0; charAnimState = CharAnimState::MOUNT; @@ -796,6 +799,9 @@ void Renderer::clearMount() { mountHeightOffset_ = 0.0f; mountPitch_ = 0.0f; mountRoll_ = 0.0f; + mountSeatAttachmentId_ = -1; + smoothedMountSeatPos_ = glm::vec3(0.0f); + mountSeatSmoothingInit_ = false; mountAction_ = MountAction::None; mountActionPhase_ = 0; charAnimState = CharAnimState::IDLE; @@ -954,6 +960,10 @@ void Renderer::updateCharacterAnimation() { if (mountInstanceId_ > 0) { characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); float yawRad = glm::radians(characterYaw); + if (taxiFlight_) { + // Taxi mounts commonly use a different model-forward axis than player rigs. + yawRad += 1.57079632679f; + } // Procedural lean into turns (ground mounts only, optional enhancement) if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { @@ -1164,22 +1174,53 @@ void Renderer::updateCharacterAnimation() { // Use mount's attachment point for proper bone-driven rider positioning glm::mat4 mountSeatTransform; - if (characterRenderer->getAttachmentTransform(mountInstanceId_, 0, mountSeatTransform)) { + bool haveSeat = false; + if (mountSeatAttachmentId_ >= 0) { + haveSeat = characterRenderer->getAttachmentTransform( + mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); + } else if (mountSeatAttachmentId_ == -1) { + // Probe common rider seat attachment IDs once per mount. + static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8}; + for (uint32_t attId : kSeatAttachments) { + if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) { + mountSeatAttachmentId_ = static_cast(attId); + haveSeat = true; + break; + } + } + if (!haveSeat) { + mountSeatAttachmentId_ = -2; + } + } + + if (haveSeat) { // Extract position from mount seat transform (attachment point already includes proper seat height) glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]); - // Apply small vertical offset to reduce foot clipping (mount attachment point has correct X/Y) - glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, 0.2f); + // Keep seat offset minimal; large offsets amplify visible bobble. + glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f); + glm::vec3 targetRiderPos = mountSeatPos + seatOffset; + if (!mountSeatSmoothingInit_) { + smoothedMountSeatPos_ = targetRiderPos; + mountSeatSmoothingInit_ = true; + } else { + float smoothHz = taxiFlight_ ? 10.0f : 14.0f; + float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f)); + smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha); + } // Position rider at mount seat - characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset); + characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_); // Rider uses character facing yaw, not mount bone rotation // (rider faces character direction, seat bone only provides position) float yawRad = glm::radians(characterYaw); - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad)); + float riderPitch = taxiFlight_ ? mountPitch_ * 0.35f : 0.0f; + float riderRoll = taxiFlight_ ? mountRoll_ * 0.35f : 0.0f; + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad)); } else { // Fallback to old manual positioning if attachment not found + mountSeatSmoothingInit_ = false; float yawRad = glm::radians(characterYaw); glm::mat4 mountRotation = glm::mat4(1.0f); mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); @@ -2385,9 +2426,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { } // Render weather particles (after terrain/water, before characters) - // Skip during taxi flights for performance and visual clarity - bool onTaxi = cameraController && cameraController->isOnTaxi(); - if (weather && camera && !onTaxi) { + if (weather && camera) { weather->render(*camera); } @@ -2430,11 +2469,8 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { } auto m2Start = std::chrono::steady_clock::now(); m2Renderer->render(*camera, view, projection); - // Skip particle fog during taxi (expensive and visually distracting) - if (!onTaxi) { - m2Renderer->renderSmokeParticles(*camera, view, projection); - m2Renderer->renderM2Particles(view, projection); - } + m2Renderer->renderSmokeParticles(*camera, view, projection); + m2Renderer->renderM2Particles(view, projection); auto m2End = std::chrono::steady_clock::now(); lastM2RenderMs = std::chrono::duration(m2End - m2Start).count(); } @@ -2807,6 +2843,23 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (questMarkerRenderer) { questMarkerRenderer->initialize(assetManager); } + + // Prewarm frequently used zone/tavern music so zone transitions don't stall on MPQ I/O. + if (zoneManager) { + for (const auto& musicPath : zoneManager->getAllMusicPaths()) { + musicManager->preloadMusic(musicPath); + } + } + static const std::vector tavernTracks = { + "Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3", + "Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3", + "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3", + "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3", + }; + for (const auto& musicPath : tavernTracks) { + musicManager->preloadMusic(musicPath); + } + cachedAssetManager = assetManager; } diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 156f8dae..288aac49 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -75,6 +75,17 @@ void WorldMap::setMapName(const std::string& name) { viewLevel = ViewLevel::WORLD; } +void WorldMap::setServerExplorationMask(const std::vector& masks, bool hasData) { + if (!hasData || masks.empty()) { + hasServerExplorationMask = false; + serverExplorationMask.clear(); + return; + } + + hasServerExplorationMask = true; + serverExplorationMask = masks; +} + // -------------------------------------------------------- // GL resource creation // -------------------------------------------------------- @@ -195,7 +206,22 @@ void WorldMap::loadZonesFromDBC() { } } - // Step 2: Load ALL WorldMapArea records for this mapID + // Step 2: Load AreaTable explore flags by areaID. + std::unordered_map exploreFlagByAreaId; + auto areaDbc = assetManager->loadDBC("AreaTable.dbc"); + if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) { + for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) { + const uint32_t areaId = areaDbc->getUInt32(i, 0); + const uint32_t exploreFlag = areaDbc->getUInt32(i, 3); + if (areaId != 0) { + exploreFlagByAreaId[areaId] = exploreFlag; + } + } + } else { + LOG_WARNING("WorldMap: AreaTable.dbc missing or unexpected format; server exploration may be incomplete"); + } + + // Step 3: Load ALL WorldMapArea records for this mapID auto wmaDbc = assetManager->loadDBC("WorldMapArea.dbc"); if (!wmaDbc || !wmaDbc->isLoaded()) { LOG_WARNING("WorldMap: WorldMapArea.dbc not found"); @@ -224,6 +250,10 @@ void WorldMap::loadZonesFromDBC() { zone.locBottom = wmaDbc->getFloat(i, 7); zone.displayMapID = wmaDbc->getUInt32(i, 8); zone.parentWorldMapID = wmaDbc->getUInt32(i, 10); + auto exploreIt = exploreFlagByAreaId.find(zone.areaID); + if (exploreIt != exploreFlagByAreaId.end()) { + zone.exploreFlag = exploreIt->second; + } int idx = static_cast(zones.size()); @@ -728,9 +758,66 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co // -------------------------------------------------------- void WorldMap::updateExploration(const glm::vec3& playerRenderPos) { - int zoneIdx = findZoneForPlayer(playerRenderPos); - if (zoneIdx >= 0) { - exploredZones.insert(zoneIdx); + auto isExploreFlagSet = [this](uint32_t flag) -> bool { + if (!hasServerExplorationMask || serverExplorationMask.empty() || flag == 0) return false; + + const auto isSet = [this](uint32_t bitIndex) -> bool { + const size_t word = bitIndex / 32; + if (word >= serverExplorationMask.size()) return false; + const uint32_t bit = bitIndex % 32; + return (serverExplorationMask[word] & (1u << bit)) != 0; + }; + + // Most cores use zero-based bit indices; some data behaves one-based. + if (isSet(flag)) return true; + if (flag > 0 && isSet(flag - 1)) return true; + return false; + }; + + bool markedAny = false; + if (hasServerExplorationMask) { + exploredZones.clear(); + for (int i = 0; i < static_cast(zones.size()); i++) { + const auto& z = zones[i]; + if (z.areaID == 0 || z.exploreFlag == 0) continue; + if (isExploreFlagSet(z.exploreFlag)) { + exploredZones.insert(i); + markedAny = true; + } + } + } + + // Fall back to local bounds-based reveal if server masks are missing/unusable. + if (markedAny) return; + + float wowX = playerRenderPos.y; // north/south + float wowY = playerRenderPos.x; // west/east + + for (int i = 0; i < static_cast(zones.size()); i++) { + const auto& z = zones[i]; + if (z.areaID == 0) continue; // skip continent-level entries + + float minX = std::min(z.locLeft, z.locRight); + float maxX = std::max(z.locLeft, z.locRight); + float minY = std::min(z.locTop, z.locBottom); + float maxY = std::max(z.locTop, z.locBottom); + float spanX = maxX - minX; + float spanY = maxY - minY; + if (spanX < 0.001f || spanY < 0.001f) continue; + + bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY); + if (contains) { + exploredZones.insert(i); + markedAny = true; + } + } + + // Fallback for imperfect DBC bounds: reveal nearest zone so exploration still progresses. + if (!markedAny) { + int zoneIdx = findZoneForPlayer(playerRenderPos); + if (zoneIdx >= 0) { + exploredZones.insert(zoneIdx); + } } } @@ -791,11 +878,16 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr return; } - // Mouse wheel: scroll up = zoom in, scroll down = zoom out + // Mouse wheel: scroll up = zoom in, scroll down = zoom out. + // Use both ImGui and raw input wheel deltas for reliability across frame order/capture paths. auto& io = ImGui::GetIO(); - if (io.MouseWheel > 0.0f) { + float wheelDelta = io.MouseWheel; + if (std::abs(wheelDelta) < 0.001f) { + wheelDelta = input.getMouseWheelDelta(); + } + if (wheelDelta > 0.0f) { zoomIn(playerRenderPos); - } else if (io.MouseWheel < 0.0f) { + } else if (wheelDelta < 0.0f) { zoomOut(); } } else { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index ba833cf6..71a32fb2 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2271,7 +2271,7 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) { // World Map // ============================================================ -void GameScreen::renderWorldMap(game::GameHandler& /* gameHandler */) { +void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { auto& app = core::Application::getInstance(); auto* renderer = app.getRenderer(); auto* assetMgr = app.getAssetManager(); @@ -2284,6 +2284,9 @@ void GameScreen::renderWorldMap(game::GameHandler& /* gameHandler */) { if (minimap) { worldMap.setMapName(minimap->getMapName()); } + worldMap.setServerExplorationMask( + gameHandler.getPlayerExploredZoneMasks(), + gameHandler.hasPlayerExploredZoneMasks()); glm::vec3 playerPos = renderer->getCharacterPosition(); auto* window = app.getWindow();