mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix NPC visibility and stabilize world transport/taxi updates
This commit is contained in:
parent
5dae994830
commit
f752a4f517
16 changed files with 452 additions and 173 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace pipeline { class AssetManager; }
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
@ -23,6 +26,7 @@ public:
|
||||||
void setVolume(int volume);
|
void setVolume(int volume);
|
||||||
int getVolume() const { return volumePercent; }
|
int getVolume() const { return volumePercent; }
|
||||||
void setUnderwaterMode(bool underwater);
|
void setUnderwaterMode(bool underwater);
|
||||||
|
void preloadMusic(const std::string& mpqPath);
|
||||||
|
|
||||||
bool isPlaying() const { return playing; }
|
bool isPlaying() const { return playing; }
|
||||||
bool isInitialized() const { return assetManager != nullptr; }
|
bool isInitialized() const { return assetManager != nullptr; }
|
||||||
|
|
@ -41,6 +45,8 @@ private:
|
||||||
std::string pendingTrack;
|
std::string pendingTrack;
|
||||||
float fadeTimer = 0.0f;
|
float fadeTimer = 0.0f;
|
||||||
float fadeDuration = 0.0f;
|
float fadeDuration = 0.0f;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::vector<uint8_t>> musicDataCache_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
||||||
|
|
@ -192,7 +193,12 @@ private:
|
||||||
float x, y, z, orientation;
|
float x, y, z, orientation;
|
||||||
};
|
};
|
||||||
std::vector<PendingCreatureSpawn> pendingCreatureSpawns_;
|
std::vector<PendingCreatureSpawn> 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<uint64_t> pendingCreatureSpawnGuids_;
|
||||||
|
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
|
||||||
|
std::unordered_set<uint32_t> nonRenderableCreatureDisplayIds_;
|
||||||
|
std::unordered_set<uint64_t> creaturePermanentFailureGuids_;
|
||||||
void processCreatureSpawnQueue();
|
void processCreatureSpawnQueue();
|
||||||
|
|
||||||
struct PendingGameObjectSpawn {
|
struct PendingGameObjectSpawn {
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,8 @@ public:
|
||||||
uint32_t getPlayerXp() const { return playerXp_; }
|
uint32_t getPlayerXp() const { return playerXp_; }
|
||||||
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
||||||
uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
|
uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
|
||||||
|
const std::vector<uint32_t>& getPlayerExploredZoneMasks() const { return playerExploredZones_; }
|
||||||
|
bool hasPlayerExploredZoneMasks() const { return hasPlayerExploredZones_; }
|
||||||
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
||||||
|
|
||||||
// Server time (for deterministic moon phases, etc.)
|
// Server time (for deterministic moon phases, etc.)
|
||||||
|
|
@ -1085,11 +1087,8 @@ private:
|
||||||
float taxiLandingCooldown_ = 0.0f; // Prevent re-entering taxi right after landing
|
float taxiLandingCooldown_ = 0.0f; // Prevent re-entering taxi right after landing
|
||||||
size_t taxiClientIndex_ = 0;
|
size_t taxiClientIndex_ = 0;
|
||||||
std::vector<glm::vec3> taxiClientPath_;
|
std::vector<glm::vec3> taxiClientPath_;
|
||||||
float taxiClientSpeed_ = 18.0f; // Reduced from 32 to prevent loading hitches
|
float taxiClientSpeed_ = 32.0f;
|
||||||
float taxiClientSegmentProgress_ = 0.0f;
|
float taxiClientSegmentProgress_ = 0.0f;
|
||||||
bool taxiMountingDelay_ = false; // Delay before flight starts (terrain precache time)
|
|
||||||
float taxiMountingTimer_ = 0.0f;
|
|
||||||
std::vector<uint32_t> taxiPendingPath_; // Path nodes waiting for mounting delay
|
|
||||||
bool taxiRecoverPending_ = false;
|
bool taxiRecoverPending_ = false;
|
||||||
uint32_t taxiRecoverMapId_ = 0;
|
uint32_t taxiRecoverMapId_ = 0;
|
||||||
glm::vec3 taxiRecoverPos_{0.0f};
|
glm::vec3 taxiRecoverPos_{0.0f};
|
||||||
|
|
@ -1144,9 +1143,15 @@ private:
|
||||||
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
|
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
|
||||||
bool skillLineDbcLoaded_ = false;
|
bool skillLineDbcLoaded_ = false;
|
||||||
bool skillLineAbilityLoaded_ = 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<uint32_t> playerExploredZones_ =
|
||||||
|
std::vector<uint32_t>(PLAYER_EXPLORED_ZONES_COUNT, 0u);
|
||||||
|
bool hasPlayerExploredZones_ = false;
|
||||||
void loadSkillLineDbc();
|
void loadSkillLineDbc();
|
||||||
void loadSkillLineAbilityDbc();
|
void loadSkillLineAbilityDbc();
|
||||||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||||||
|
void extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields);
|
||||||
|
|
||||||
NpcDeathCallback npcDeathCallback_;
|
NpcDeathCallback npcDeathCallback_;
|
||||||
NpcAggroCallback npcAggroCallback_;
|
NpcAggroCallback npcAggroCallback_;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public:
|
||||||
uint32_t getZoneId(int tileX, int tileY) const;
|
uint32_t getZoneId(int tileX, int tileY) const;
|
||||||
const ZoneInfo* getZoneInfo(uint32_t zoneId) const;
|
const ZoneInfo* getZoneInfo(uint32_t zoneId) const;
|
||||||
std::string getRandomMusic(uint32_t zoneId) const;
|
std::string getRandomMusic(uint32_t zoneId) const;
|
||||||
|
std::vector<std::string> getAllMusicPaths() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// tile key = tileX * 100 + tileY
|
// tile key = tileX * 100 + tileY
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,9 @@ private:
|
||||||
float mountHeightOffset_ = 0.0f;
|
float mountHeightOffset_ = 0.0f;
|
||||||
float mountPitch_ = 0.0f; // Up/down tilt (radians)
|
float mountPitch_ = 0.0f; // Up/down tilt (radians)
|
||||||
float mountRoll_ = 0.0f; // Left/right banking (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 prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean)
|
||||||
float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation()
|
float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation()
|
||||||
MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up)
|
MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ struct WorldMapZone {
|
||||||
float locLeft = 0, locRight = 0, locTop = 0, locBottom = 0;
|
float locLeft = 0, locRight = 0, locTop = 0, locBottom = 0;
|
||||||
uint32_t displayMapID = 0;
|
uint32_t displayMapID = 0;
|
||||||
uint32_t parentWorldMapID = 0;
|
uint32_t parentWorldMapID = 0;
|
||||||
|
uint32_t exploreFlag = 0;
|
||||||
|
|
||||||
// Per-zone cached textures
|
// Per-zone cached textures
|
||||||
GLuint tileTextures[12] = {};
|
GLuint tileTextures[12] = {};
|
||||||
|
|
@ -34,6 +35,7 @@ public:
|
||||||
void initialize(pipeline::AssetManager* assetManager);
|
void initialize(pipeline::AssetManager* assetManager);
|
||||||
void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight);
|
void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight);
|
||||||
void setMapName(const std::string& name);
|
void setMapName(const std::string& name);
|
||||||
|
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||||
bool isOpen() const { return open; }
|
bool isOpen() const { return open; }
|
||||||
void close() { open = false; }
|
void close() { open = false; }
|
||||||
|
|
||||||
|
|
@ -87,6 +89,8 @@ private:
|
||||||
GLuint tileQuadVBO = 0;
|
GLuint tileQuadVBO = 0;
|
||||||
|
|
||||||
// Exploration / fog of war
|
// Exploration / fog of war
|
||||||
|
std::vector<uint32_t> serverExplorationMask;
|
||||||
|
bool hasServerExplorationMask = false;
|
||||||
std::unordered_set<int> exploredZones; // zone indices the player has visited
|
std::unordered_set<int> exploredZones; // zone indices the player has visited
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ void MusicManager::shutdown() {
|
||||||
AudioEngine::instance().stopMusic();
|
AudioEngine::instance().stopMusic();
|
||||||
playing = false;
|
playing = false;
|
||||||
currentTrack.clear();
|
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) {
|
void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
|
||||||
|
|
@ -36,9 +47,13 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read music file from MPQ
|
// Read music file from cache or MPQ
|
||||||
auto data = assetManager->readFile(mpqPath);
|
auto cacheIt = musicDataCache_.find(mpqPath);
|
||||||
if (data.empty()) {
|
if (cacheIt == musicDataCache_.end()) {
|
||||||
|
preloadMusic(mpqPath);
|
||||||
|
cacheIt = musicDataCache_.find(mpqPath);
|
||||||
|
}
|
||||||
|
if (cacheIt == musicDataCache_.end() || cacheIt->second.empty()) {
|
||||||
LOG_WARNING("Music: Could not read: ", mpqPath);
|
LOG_WARNING("Music: Could not read: ", mpqPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +63,7 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
|
||||||
|
|
||||||
// Play with AudioEngine (non-blocking, streams from memory)
|
// Play with AudioEngine (non-blocking, streams from memory)
|
||||||
float volume = volumePercent / 100.0f;
|
float volume = volumePercent / 100.0f;
|
||||||
if (AudioEngine::instance().playMusic(data, volume, loop)) {
|
if (AudioEngine::instance().playMusic(cacheIt->second, volume, loop)) {
|
||||||
playing = true;
|
playing = true;
|
||||||
currentTrack = mpqPath;
|
currentTrack = mpqPath;
|
||||||
currentTrackIsFile = false;
|
currentTrackIsFile = false;
|
||||||
|
|
|
||||||
|
|
@ -467,23 +467,7 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
if (renderer && renderer->getTerrainManager()) {
|
if (renderer && renderer->getTerrainManager()) {
|
||||||
renderer->getTerrainManager()->setStreamingEnabled(true);
|
renderer->getTerrainManager()->setStreamingEnabled(true);
|
||||||
// With 8GB tile cache and precaching, minimize streaming during taxi
|
renderer->getTerrainManager()->setUpdateInterval(0.1f);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
lastTaxiFlight_ = onTaxi;
|
lastTaxiFlight_ = onTaxi;
|
||||||
|
|
||||||
|
|
@ -760,8 +744,12 @@ void Application::setupUICallbacks() {
|
||||||
|
|
||||||
// Creature spawn callback (online mode) - spawn creature models
|
// 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) {
|
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});
|
pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation});
|
||||||
|
pendingCreatureSpawnGuids_.insert(guid);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creature despawn callback (online mode) - remove creature models
|
// 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
|
// Skip if already spawned
|
||||||
if (creatureInstances_.count(guid)) return;
|
if (creatureInstances_.count(guid)) return;
|
||||||
|
|
||||||
|
if (nonRenderableCreatureDisplayIds_.count(displayId)) {
|
||||||
|
creaturePermanentFailureGuids_.insert(guid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get model path from displayId
|
// Get model path from displayId
|
||||||
std::string m2Path = getModelPathForDisplayId(displayId);
|
std::string m2Path = getModelPathForDisplayId(displayId);
|
||||||
if (m2Path.empty()) {
|
if (m2Path.empty()) {
|
||||||
LOG_WARNING("No model path for displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")");
|
LOG_WARNING("No model path for displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")");
|
||||||
|
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||||
|
creaturePermanentFailureGuids_.insert(guid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2321,12 +2316,15 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
auto m2Data = assetManager->readFile(m2Path);
|
auto m2Data = assetManager->readFile(m2Path);
|
||||||
if (m2Data.empty()) {
|
if (m2Data.empty()) {
|
||||||
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
||||||
|
creaturePermanentFailureGuids_.insert(guid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||||
if (model.vertices.empty()) {
|
if (model.vertices.empty()) {
|
||||||
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
||||||
|
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||||
|
creaturePermanentFailureGuids_.insert(guid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2353,6 +2351,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
|
|
||||||
if (!charRenderer->loadModel(model, modelId)) {
|
if (!charRenderer->loadModel(model, modelId)) {
|
||||||
LOG_WARNING("Failed to load creature model: ", m2Path);
|
LOG_WARNING("Failed to load creature model: ", m2Path);
|
||||||
|
creaturePermanentFailureGuids_.insert(guid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2497,7 +2496,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
// Smart filtering for bad spawn data:
|
// Smart filtering for bad spawn data:
|
||||||
// - If over ocean AND at continental height (Z > 50): bad data, skip
|
// - 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 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()) {
|
if (renderer->getTerrainManager()) {
|
||||||
auto terrainH = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
|
auto terrainH = renderer->getTerrainManager()->getHeightAt(renderPos.x, renderPos.y);
|
||||||
if (!terrainH) {
|
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
|
// Low altitude = probably legitimate water creature, allow spawn at original Z
|
||||||
} else {
|
} else {
|
||||||
// Valid terrain found - snap to terrain height
|
// Keep authentic server Z for NPCs on raised geometry (e.g. flight masters).
|
||||||
renderPos.z = *terrainH + 0.1f;
|
// 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
|
// Track instance
|
||||||
creatureInstances_[guid] = instanceId;
|
creatureInstances_[guid] = instanceId;
|
||||||
creatureModelIds_[guid] = modelId;
|
creatureModelIds_[guid] = modelId;
|
||||||
|
creaturePermanentFailureGuids_.erase(guid);
|
||||||
|
|
||||||
LOG_INFO("Spawned creature: guid=0x", std::hex, guid, std::dec,
|
LOG_INFO("Spawned creature: guid=0x", std::hex, guid, std::dec,
|
||||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||||
|
|
@ -3051,13 +3055,43 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||||
|
|
||||||
void Application::processCreatureSpawnQueue() {
|
void Application::processCreatureSpawnQueue() {
|
||||||
if (pendingCreatureSpawns_.empty()) return;
|
if (pendingCreatureSpawns_.empty()) return;
|
||||||
|
if (!creatureLookupsBuilt_) {
|
||||||
|
buildCreatureDisplayLookups();
|
||||||
|
if (!creatureLookupsBuilt_) return;
|
||||||
|
}
|
||||||
|
|
||||||
int spawned = 0;
|
int processed = 0;
|
||||||
while (!pendingCreatureSpawns_.empty() && spawned < MAX_SPAWNS_PER_FRAME) {
|
while (!pendingCreatureSpawns_.empty() && processed < MAX_SPAWNS_PER_FRAME) {
|
||||||
auto& s = pendingCreatureSpawns_.front();
|
PendingCreatureSpawn s = pendingCreatureSpawns_.front();
|
||||||
spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation);
|
spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||||
pendingCreatureSpawns_.erase(pendingCreatureSpawns_.begin());
|
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<uint8_t>(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
|
// Check model cache
|
||||||
uint32_t modelId = 0;
|
uint32_t modelId = 0;
|
||||||
bool modelCached = false;
|
|
||||||
auto cacheIt = displayIdModelCache_.find(mountDisplayId);
|
auto cacheIt = displayIdModelCache_.find(mountDisplayId);
|
||||||
if (cacheIt != displayIdModelCache_.end()) {
|
if (cacheIt != displayIdModelCache_.end()) {
|
||||||
modelId = cacheIt->second;
|
modelId = cacheIt->second;
|
||||||
modelCached = true;
|
|
||||||
} else {
|
} else {
|
||||||
modelId = nextCreatureModelId_++;
|
modelId = nextCreatureModelId_++;
|
||||||
|
|
||||||
|
|
@ -3142,73 +3174,72 @@ void Application::processPendingMount() {
|
||||||
displayIdModelCache_[mountDisplayId] = modelId;
|
displayIdModelCache_[mountDisplayId] = modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply creature skin textures from CreatureDisplayInfo.dbc
|
// Apply creature skin textures from CreatureDisplayInfo.dbc.
|
||||||
if (!modelCached) {
|
// Re-apply even for cached models so transient failures can self-heal.
|
||||||
auto itDisplayData = displayDataMap_.find(mountDisplayId);
|
auto itDisplayData = displayDataMap_.find(mountDisplayId);
|
||||||
if (itDisplayData != displayDataMap_.end()) {
|
if (itDisplayData != displayDataMap_.end()) {
|
||||||
CreatureDisplayData 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 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()) {
|
if (dispData.skin1.empty() && dispData.skin2.empty() && dispData.skin3.empty()) {
|
||||||
uint32_t modelId = dispData.modelId;
|
uint32_t modelId = dispData.modelId;
|
||||||
int bestScore = -1;
|
int bestScore = -1;
|
||||||
for (const auto& [dispId, data] : displayDataMap_) {
|
for (const auto& [dispId, data] : displayDataMap_) {
|
||||||
if (data.modelId != modelId) continue;
|
if (data.modelId != modelId) continue;
|
||||||
int score = 0;
|
int score = 0;
|
||||||
if (!data.skin1.empty()) score += 3;
|
if (!data.skin1.empty()) score += 3;
|
||||||
if (!data.skin2.empty()) score += 2;
|
if (!data.skin2.empty()) score += 2;
|
||||||
if (!data.skin3.empty()) score += 1;
|
if (!data.skin3.empty()) score += 1;
|
||||||
if (score > bestScore) {
|
if (score > bestScore) {
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
dispData = data;
|
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);
|
LOG_INFO("Mount skin fallback for displayId=", mountDisplayId,
|
||||||
if (md) {
|
" modelId=", modelId, " skin1='", dispData.skin1,
|
||||||
std::string modelDir;
|
"' skin2='", dispData.skin2, "' skin3='", dispData.skin3, "'");
|
||||||
size_t lastSlash = m2Path.find_last_of("\\/");
|
}
|
||||||
if (lastSlash != std::string::npos) {
|
const auto* md = charRenderer->getModelData(modelId);
|
||||||
modelDir = m2Path.substr(0, lastSlash + 1);
|
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;
|
if (!texPath.empty()) {
|
||||||
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<uint32_t>(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);
|
GLuint skinTex = charRenderer->loadTexture(texPath);
|
||||||
if (skinTex != 0) {
|
if (skinTex != 0) {
|
||||||
charRenderer->setModelTexture(modelId, 0, skinTex);
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), skinTex);
|
||||||
LOG_INFO("Forced mount skin1 texture on slot 0: ", texPath);
|
replaced++;
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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) {
|
void Application::despawnOnlineCreature(uint64_t guid) {
|
||||||
|
pendingCreatureSpawnGuids_.erase(guid);
|
||||||
|
creatureSpawnRetryCounts_.erase(guid);
|
||||||
|
creaturePermanentFailureGuids_.erase(guid);
|
||||||
|
|
||||||
auto it = creatureInstances_.find(guid);
|
auto it = creatureInstances_.find(guid);
|
||||||
if (it == creatureInstances_.end()) return;
|
if (it == creatureInstances_.end()) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
auto taxiEnd = std::chrono::high_resolution_clock::now();
|
||||||
taxiTime += std::chrono::duration<float, std::milli>(taxiEnd - taxiStart).count();
|
taxiTime += std::chrono::duration<float, std::milli>(taxiEnd - taxiStart).count();
|
||||||
|
|
||||||
|
|
@ -1524,6 +1506,8 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||||
playerXp_ = 0;
|
playerXp_ = 0;
|
||||||
playerNextLevelXp_ = 0;
|
playerNextLevelXp_ = 0;
|
||||||
serverPlayerLevel_ = 1;
|
serverPlayerLevel_ = 1;
|
||||||
|
std::fill(playerExploredZones_.begin(), playerExploredZones_.end(), 0u);
|
||||||
|
hasPlayerExploredZones_ = false;
|
||||||
playerSkills_.clear();
|
playerSkills_.clear();
|
||||||
questLog_.clear();
|
questLog_.clear();
|
||||||
npcQuestStatus_.clear();
|
npcQuestStatus_.clear();
|
||||||
|
|
@ -2235,6 +2219,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
||||||
if (slotsChanged) rebuildOnlineInventory();
|
if (slotsChanged) rebuildOnlineInventory();
|
||||||
extractSkillFields(lastPlayerFields_);
|
extractSkillFields(lastPlayerFields_);
|
||||||
|
extractExploredZoneFields(lastPlayerFields_);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -2251,6 +2236,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
|
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
|
||||||
|
uint32_t oldDisplayId = unit->getDisplayId();
|
||||||
|
bool displayIdChanged = false;
|
||||||
for (const auto& [key, val] : block.fields) {
|
for (const auto& [key, val] : block.fields) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 24: {
|
case 24: {
|
||||||
|
|
@ -2316,7 +2303,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
unit->setFactionTemplate(val);
|
unit->setFactionTemplate(val);
|
||||||
unit->setHostile(isHostileFaction(val));
|
unit->setHostile(isHostileFaction(val));
|
||||||
break;
|
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
|
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||||
if (block.guid == playerGuid) {
|
if (block.guid == playerGuid) {
|
||||||
uint32_t old = currentMountDisplayId_;
|
uint32_t old = currentMountDisplayId_;
|
||||||
|
|
@ -2333,6 +2325,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
default: break;
|
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<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||||
|
qsPkt.writeUInt64(block.guid);
|
||||||
|
socket->send(qsPkt);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update XP / inventory slot / skill fields for player entity
|
// Update XP / inventory slot / skill fields for player entity
|
||||||
if (block.guid == playerGuid) {
|
if (block.guid == playerGuid) {
|
||||||
|
|
@ -2387,6 +2395,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
||||||
if (slotsChanged) rebuildOnlineInventory();
|
if (slotsChanged) rebuildOnlineInventory();
|
||||||
extractSkillFields(lastPlayerFields_);
|
extractSkillFields(lastPlayerFields_);
|
||||||
|
extractExploredZoneFields(lastPlayerFields_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update item stack count for online items
|
// Update item stack count for online items
|
||||||
|
|
@ -6125,10 +6134,12 @@ void GameHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
|
||||||
glm::vec3 start = taxiClientPath_[0];
|
glm::vec3 start = taxiClientPath_[0];
|
||||||
glm::vec3 end = taxiClientPath_[1];
|
glm::vec3 end = taxiClientPath_[1];
|
||||||
glm::vec3 dir = end - start;
|
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;
|
float initialOrientation = std::atan2(dir.y, dir.x) - 1.57079632679f;
|
||||||
|
|
||||||
// Calculate initial pitch from altitude change
|
// 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 initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f));
|
||||||
float initialRoll = 0.0f; // No initial banking
|
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 +
|
2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t +
|
||||||
3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2
|
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
|
// Calculate yaw from horizontal direction
|
||||||
float targetOrientation = std::atan2(tangent.y, tangent.x) - 1.57079632679f;
|
float targetOrientation = std::atan2(tangent.y, tangent.x) - 1.57079632679f;
|
||||||
|
|
||||||
// Calculate pitch from vertical component (altitude change)
|
// 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));
|
float pitch = std::asin(std::clamp(tangentNorm.z, -1.0f, 1.0f));
|
||||||
|
|
||||||
// Calculate roll (banking) from rate of yaw change
|
// Calculate roll (banking) from rate of yaw change
|
||||||
|
|
@ -6419,12 +6439,7 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
|
||||||
applyTaxiMountForCurrentNode();
|
applyTaxiMountForCurrentNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start mounting delay (gives terrain precache time to load)
|
// Trigger terrain precache immediately (non-blocking).
|
||||||
taxiMountingDelay_ = true;
|
|
||||||
taxiMountingTimer_ = 0.0f;
|
|
||||||
taxiPendingPath_ = path;
|
|
||||||
|
|
||||||
// Trigger terrain precache immediately (uses mounting delay time to load)
|
|
||||||
if (taxiPrecacheCallback_) {
|
if (taxiPrecacheCallback_) {
|
||||||
std::vector<glm::vec3> previewPath;
|
std::vector<glm::vec3> previewPath;
|
||||||
// Build full spline path using TaxiPathNode waypoints
|
// 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.
|
// Save recovery target in case of disconnect during taxi.
|
||||||
auto destIt = taxiNodes_.find(destNodeId);
|
auto destIt = taxiNodes_.find(destNodeId);
|
||||||
|
|
@ -6804,6 +6825,25 @@ void GameHandler::extractSkillFields(const std::map<uint16_t, uint32_t>& fields)
|
||||||
playerSkills_ = std::move(newSkills);
|
playerSkills_ = std::move(newSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::extractExploredZoneFields(const std::map<uint16_t, uint32_t>& 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<uint16_t>(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 GameHandler::getCharacterConfigDir() {
|
||||||
std::string dir;
|
std::string dir;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|
@ -112,5 +113,20 @@ std::string ZoneManager::getRandomMusic(uint32_t zoneId) const {
|
||||||
return paths[std::rand() % paths.size()];
|
return paths[std::rand() % paths.size()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> ZoneManager::getAllMusicPaths() const {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::unordered_set<std::string> 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 game
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,8 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
|
||||||
|
|
||||||
LOG_DEBUG("Loading texture: ", normalizedPath);
|
LOG_DEBUG("Loading texture: ", normalizedPath);
|
||||||
|
|
||||||
// Read BLP file from MPQ (must hold readMutex — StormLib is not thread-safe)
|
// Route through readFile() so decompressed MPQ bytes participate in file cache.
|
||||||
std::vector<uint8_t> blpData;
|
std::vector<uint8_t> blpData = readFile(normalizedPath);
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(readMutex);
|
|
||||||
blpData = mpqManager.readFile(normalizedPath);
|
|
||||||
}
|
|
||||||
if (blpData.empty()) {
|
if (blpData.empty()) {
|
||||||
LOG_WARNING("Texture not found: ", normalizedPath);
|
LOG_WARNING("Texture not found: ", normalizedPath);
|
||||||
return BLPImage();
|
return BLPImage();
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,8 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
auto blpImage = assetManager->loadTexture(path);
|
auto blpImage = assetManager->loadTexture(path);
|
||||||
if (!blpImage.isValid()) {
|
if (!blpImage.isValid()) {
|
||||||
core::Logger::getInstance().warning("Failed to load texture: ", path);
|
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;
|
return whiteTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1257,6 +1258,26 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
glBindVertexArray(gpuModel.vao);
|
glBindVertexArray(gpuModel.vao);
|
||||||
|
|
||||||
if (!gpuModel.data.batches.empty()) {
|
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<uint32_t> 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
|
// One-time debug dump of rendered batches per model
|
||||||
static std::unordered_set<uint32_t> dumpedModels;
|
static std::unordered_set<uint32_t> dumpedModels;
|
||||||
if (dumpedModels.find(instance.modelId) == dumpedModels.end()) {
|
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 bIdx = 0;
|
||||||
int rendered = 0, skipped = 0;
|
int rendered = 0, skipped = 0;
|
||||||
for (const auto& b : gpuModel.data.batches) {
|
for (const auto& b : gpuModel.data.batches) {
|
||||||
bool filtered = !instance.activeGeosets.empty() &&
|
bool filtered = applyGeosetFilter &&
|
||||||
(b.submeshId / 100 != 0) &&
|
(b.submeshId / 100 != 0) &&
|
||||||
instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end();
|
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
|
// For character models, group 0 (body/scalp) is also filtered so that only
|
||||||
// the correct scalp mesh renders (not all overlapping variants).
|
// the correct scalp mesh renders (not all overlapping variants).
|
||||||
for (const auto& batch : gpuModel.data.batches) {
|
for (const auto& batch : gpuModel.data.batches) {
|
||||||
if (!instance.activeGeosets.empty()) {
|
if (applyGeosetFilter) {
|
||||||
if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) {
|
if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1661,14 +1661,13 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
shader->setUniform("uProjection", projection);
|
shader->setUniform("uProjection", projection);
|
||||||
shader->setUniform("uLightDir", lightDir);
|
shader->setUniform("uLightDir", lightDir);
|
||||||
shader->setUniform("uLightColor", lightColor);
|
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("uAmbientColor", ambientColor);
|
||||||
shader->setUniform("uViewPos", camera.getPosition());
|
shader->setUniform("uViewPos", camera.getPosition());
|
||||||
shader->setUniform("uFogColor", fogColor);
|
shader->setUniform("uFogColor", fogColor);
|
||||||
shader->setUniform("uFogStart", fogStart);
|
shader->setUniform("uFogStart", fogStart);
|
||||||
shader->setUniform("uFogEnd", fogEnd);
|
shader->setUniform("uFogEnd", fogEnd);
|
||||||
// Disable shadows during taxi for better performance
|
bool useShadows = shadowEnabled;
|
||||||
bool useShadows = shadowEnabled && !onTaxi_;
|
|
||||||
shader->setUniform("uShadowEnabled", useShadows ? 1 : 0);
|
shader->setUniform("uShadowEnabled", useShadows ? 1 : 0);
|
||||||
shader->setUniform("uShadowStrength", 0.65f);
|
shader->setUniform("uShadowStrength", 0.65f);
|
||||||
if (useShadows) {
|
if (useShadows) {
|
||||||
|
|
@ -1681,7 +1680,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
lastDrawCallCount = 0;
|
lastDrawCallCount = 0;
|
||||||
|
|
||||||
// Adaptive render distance: balanced for performance without excessive pop-in
|
// 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 maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||||
const float fadeStartFraction = 0.75f;
|
const float fadeStartFraction = 0.75f;
|
||||||
const glm::vec3 camPos = camera.getPosition();
|
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;
|
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)
|
// Distance-based fade alpha for smooth pop-in (squared-distance, no sqrt)
|
||||||
float fadeAlpha = 1.0f;
|
float fadeAlpha = 1.0f;
|
||||||
float fadeFrac = model.disableAnimation ? 0.55f : fadeStartFraction;
|
float fadeFrac = model.disableAnimation ? 0.55f : fadeStartFraction;
|
||||||
|
|
|
||||||
|
|
@ -557,6 +557,9 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
||||||
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
||||||
mountInstanceId_ = mountInstId;
|
mountInstanceId_ = mountInstId;
|
||||||
mountHeightOffset_ = heightOffset;
|
mountHeightOffset_ = heightOffset;
|
||||||
|
mountSeatAttachmentId_ = -1;
|
||||||
|
smoothedMountSeatPos_ = characterPosition;
|
||||||
|
mountSeatSmoothingInit_ = false;
|
||||||
mountAction_ = MountAction::None; // Clear mount action state
|
mountAction_ = MountAction::None; // Clear mount action state
|
||||||
mountActionPhase_ = 0;
|
mountActionPhase_ = 0;
|
||||||
charAnimState = CharAnimState::MOUNT;
|
charAnimState = CharAnimState::MOUNT;
|
||||||
|
|
@ -796,6 +799,9 @@ void Renderer::clearMount() {
|
||||||
mountHeightOffset_ = 0.0f;
|
mountHeightOffset_ = 0.0f;
|
||||||
mountPitch_ = 0.0f;
|
mountPitch_ = 0.0f;
|
||||||
mountRoll_ = 0.0f;
|
mountRoll_ = 0.0f;
|
||||||
|
mountSeatAttachmentId_ = -1;
|
||||||
|
smoothedMountSeatPos_ = glm::vec3(0.0f);
|
||||||
|
mountSeatSmoothingInit_ = false;
|
||||||
mountAction_ = MountAction::None;
|
mountAction_ = MountAction::None;
|
||||||
mountActionPhase_ = 0;
|
mountActionPhase_ = 0;
|
||||||
charAnimState = CharAnimState::IDLE;
|
charAnimState = CharAnimState::IDLE;
|
||||||
|
|
@ -954,6 +960,10 @@ void Renderer::updateCharacterAnimation() {
|
||||||
if (mountInstanceId_ > 0) {
|
if (mountInstanceId_ > 0) {
|
||||||
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
||||||
float yawRad = glm::radians(characterYaw);
|
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)
|
// Procedural lean into turns (ground mounts only, optional enhancement)
|
||||||
if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) {
|
if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) {
|
||||||
|
|
@ -1164,22 +1174,53 @@ void Renderer::updateCharacterAnimation() {
|
||||||
|
|
||||||
// Use mount's attachment point for proper bone-driven rider positioning
|
// Use mount's attachment point for proper bone-driven rider positioning
|
||||||
glm::mat4 mountSeatTransform;
|
glm::mat4 mountSeatTransform;
|
||||||
if (characterRenderer->getAttachmentTransform(mountInstanceId_, 0, mountSeatTransform)) {
|
bool haveSeat = false;
|
||||||
|
if (mountSeatAttachmentId_ >= 0) {
|
||||||
|
haveSeat = characterRenderer->getAttachmentTransform(
|
||||||
|
mountInstanceId_, static_cast<uint32_t>(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<int>(attId);
|
||||||
|
haveSeat = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!haveSeat) {
|
||||||
|
mountSeatAttachmentId_ = -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (haveSeat) {
|
||||||
// Extract position from mount seat transform (attachment point already includes proper seat height)
|
// Extract position from mount seat transform (attachment point already includes proper seat height)
|
||||||
glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]);
|
glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]);
|
||||||
|
|
||||||
// Apply small vertical offset to reduce foot clipping (mount attachment point has correct X/Y)
|
// Keep seat offset minimal; large offsets amplify visible bobble.
|
||||||
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, 0.2f);
|
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
|
// Position rider at mount seat
|
||||||
characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset);
|
characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_);
|
||||||
|
|
||||||
// Rider uses character facing yaw, not mount bone rotation
|
// Rider uses character facing yaw, not mount bone rotation
|
||||||
// (rider faces character direction, seat bone only provides position)
|
// (rider faces character direction, seat bone only provides position)
|
||||||
float yawRad = glm::radians(characterYaw);
|
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 {
|
} else {
|
||||||
// Fallback to old manual positioning if attachment not found
|
// Fallback to old manual positioning if attachment not found
|
||||||
|
mountSeatSmoothingInit_ = false;
|
||||||
float yawRad = glm::radians(characterYaw);
|
float yawRad = glm::radians(characterYaw);
|
||||||
glm::mat4 mountRotation = glm::mat4(1.0f);
|
glm::mat4 mountRotation = glm::mat4(1.0f);
|
||||||
mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 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)
|
// Render weather particles (after terrain/water, before characters)
|
||||||
// Skip during taxi flights for performance and visual clarity
|
if (weather && camera) {
|
||||||
bool onTaxi = cameraController && cameraController->isOnTaxi();
|
|
||||||
if (weather && camera && !onTaxi) {
|
|
||||||
weather->render(*camera);
|
weather->render(*camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2430,11 +2469,8 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
}
|
}
|
||||||
auto m2Start = std::chrono::steady_clock::now();
|
auto m2Start = std::chrono::steady_clock::now();
|
||||||
m2Renderer->render(*camera, view, projection);
|
m2Renderer->render(*camera, view, projection);
|
||||||
// Skip particle fog during taxi (expensive and visually distracting)
|
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
||||||
if (!onTaxi) {
|
m2Renderer->renderM2Particles(view, projection);
|
||||||
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
|
||||||
m2Renderer->renderM2Particles(view, projection);
|
|
||||||
}
|
|
||||||
auto m2End = std::chrono::steady_clock::now();
|
auto m2End = std::chrono::steady_clock::now();
|
||||||
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
||||||
}
|
}
|
||||||
|
|
@ -2807,6 +2843,23 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
||||||
if (questMarkerRenderer) {
|
if (questMarkerRenderer) {
|
||||||
questMarkerRenderer->initialize(assetManager);
|
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<std::string> 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;
|
cachedAssetManager = assetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,17 @@ void WorldMap::setMapName(const std::string& name) {
|
||||||
viewLevel = ViewLevel::WORLD;
|
viewLevel = ViewLevel::WORLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WorldMap::setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData) {
|
||||||
|
if (!hasData || masks.empty()) {
|
||||||
|
hasServerExplorationMask = false;
|
||||||
|
serverExplorationMask.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasServerExplorationMask = true;
|
||||||
|
serverExplorationMask = masks;
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// GL resource creation
|
// 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<uint32_t, uint32_t> 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");
|
auto wmaDbc = assetManager->loadDBC("WorldMapArea.dbc");
|
||||||
if (!wmaDbc || !wmaDbc->isLoaded()) {
|
if (!wmaDbc || !wmaDbc->isLoaded()) {
|
||||||
LOG_WARNING("WorldMap: WorldMapArea.dbc not found");
|
LOG_WARNING("WorldMap: WorldMapArea.dbc not found");
|
||||||
|
|
@ -224,6 +250,10 @@ void WorldMap::loadZonesFromDBC() {
|
||||||
zone.locBottom = wmaDbc->getFloat(i, 7);
|
zone.locBottom = wmaDbc->getFloat(i, 7);
|
||||||
zone.displayMapID = wmaDbc->getUInt32(i, 8);
|
zone.displayMapID = wmaDbc->getUInt32(i, 8);
|
||||||
zone.parentWorldMapID = wmaDbc->getUInt32(i, 10);
|
zone.parentWorldMapID = wmaDbc->getUInt32(i, 10);
|
||||||
|
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
|
||||||
|
if (exploreIt != exploreFlagByAreaId.end()) {
|
||||||
|
zone.exploreFlag = exploreIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
int idx = static_cast<int>(zones.size());
|
int idx = static_cast<int>(zones.size());
|
||||||
|
|
||||||
|
|
@ -728,9 +758,66 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
void WorldMap::updateExploration(const glm::vec3& playerRenderPos) {
|
void WorldMap::updateExploration(const glm::vec3& playerRenderPos) {
|
||||||
int zoneIdx = findZoneForPlayer(playerRenderPos);
|
auto isExploreFlagSet = [this](uint32_t flag) -> bool {
|
||||||
if (zoneIdx >= 0) {
|
if (!hasServerExplorationMask || serverExplorationMask.empty() || flag == 0) return false;
|
||||||
exploredZones.insert(zoneIdx);
|
|
||||||
|
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<int>(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<int>(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;
|
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();
|
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);
|
zoomIn(playerRenderPos);
|
||||||
} else if (io.MouseWheel < 0.0f) {
|
} else if (wheelDelta < 0.0f) {
|
||||||
zoomOut();
|
zoomOut();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -2271,7 +2271,7 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
||||||
// World Map
|
// World Map
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void GameScreen::renderWorldMap(game::GameHandler& /* gameHandler */) {
|
void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
|
||||||
auto& app = core::Application::getInstance();
|
auto& app = core::Application::getInstance();
|
||||||
auto* renderer = app.getRenderer();
|
auto* renderer = app.getRenderer();
|
||||||
auto* assetMgr = app.getAssetManager();
|
auto* assetMgr = app.getAssetManager();
|
||||||
|
|
@ -2284,6 +2284,9 @@ void GameScreen::renderWorldMap(game::GameHandler& /* gameHandler */) {
|
||||||
if (minimap) {
|
if (minimap) {
|
||||||
worldMap.setMapName(minimap->getMapName());
|
worldMap.setMapName(minimap->getMapName());
|
||||||
}
|
}
|
||||||
|
worldMap.setServerExplorationMask(
|
||||||
|
gameHandler.getPlayerExploredZoneMasks(),
|
||||||
|
gameHandler.hasPlayerExploredZoneMasks());
|
||||||
|
|
||||||
glm::vec3 playerPos = renderer->getCharacterPosition();
|
glm::vec3 playerPos = renderer->getCharacterPosition();
|
||||||
auto* window = app.getWindow();
|
auto* window = app.getWindow();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue