From afeaa13562ca284e81eeb7580592875c1493f379 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 1 Apr 2026 13:31:48 +0300 Subject: [PATCH 1/4] chore(application): extract entity spawner + composer, apply app and UI updates - add include/core/appearance_composer.hpp + src/core/appearance_composer.cpp - update include/core/application.hpp + src/core/application.cpp - update src/ui/game_screen.cpp - adjust CMakeLists.txt and README.md for new composer module --- CMakeLists.txt | 1 + README.md | 10 + include/core/appearance_composer.hpp | 101 +++++++ include/core/application.hpp | 29 +- src/core/appearance_composer.cpp | 383 ++++++++++++++++++++++++++ src/core/application.cpp | 393 +++------------------------ src/ui/game_screen.cpp | 2 +- 7 files changed, 544 insertions(+), 375 deletions(-) create mode 100644 include/core/appearance_composer.hpp create mode 100644 src/core/appearance_composer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 432c0f65..101a92cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -487,6 +487,7 @@ set(WOWEE_SOURCES # Core src/core/application.cpp src/core/entity_spawner.cpp + src/core/appearance_composer.cpp src/core/window.cpp src/core/input.cpp src/core/logger.cpp diff --git a/README.md b/README.md index 4aeffc18..69e0a6dd 100644 --- a/README.md +++ b/README.md @@ -347,3 +347,13 @@ This project does not include any Blizzard Entertainment proprietary data, asset ## Known Issues MANY issues this is actively under development + +## Star History + + + + + + Star History Chart + + diff --git a/include/core/appearance_composer.hpp b/include/core/appearance_composer.hpp new file mode 100644 index 00000000..138fd800 --- /dev/null +++ b/include/core/appearance_composer.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "game/character.hpp" +#include +#include +#include +#include + +namespace wowee { + +namespace rendering { class Renderer; } +namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; } +namespace game { class GameHandler; } + +namespace core { + +class EntitySpawner; + +// Default (bare) geoset IDs per equipment group. +// Each group's base is groupNumber * 100; variant 01 is typically bare/default. +constexpr uint16_t kGeosetDefaultConnector = 101; // Group 1: default hair connector +constexpr uint16_t kGeosetBareForearms = 401; // Group 4: no gloves +constexpr uint16_t kGeosetBareShins = 503; // Group 5: no boots +constexpr uint16_t kGeosetDefaultEars = 702; // Group 7: ears +constexpr uint16_t kGeosetBareSleeves = 801; // Group 8: no chest armor sleeves +constexpr uint16_t kGeosetDefaultKneepads = 902; // Group 9: kneepads +constexpr uint16_t kGeosetDefaultTabard = 1201; // Group 12: tabard base +constexpr uint16_t kGeosetBarePants = 1301; // Group 13: no leggings +constexpr uint16_t kGeosetNoCape = 1501; // Group 15: no cape +constexpr uint16_t kGeosetWithCape = 1502; // Group 15: with cape +constexpr uint16_t kGeosetBareFeet = 2002; // Group 20: bare feet + +/// Resolved texture paths from CharSections.dbc for player character compositing. +struct PlayerTextureInfo { + std::string bodySkinPath; + std::string faceLowerPath; + std::string faceUpperPath; + std::string hairTexturePath; + std::vector underwearPaths; +}; + +/// Handles player character visual appearance: skin compositing, geoset selection, +/// texture path lookups, and equipment weapon rendering. +class AppearanceComposer { +public: + AppearanceComposer(rendering::Renderer* renderer, + pipeline::AssetManager* assetManager, + game::GameHandler* gameHandler, + pipeline::DBCLayout* dbcLayout, + EntitySpawner* entitySpawner); + + // Player model path resolution + std::string getPlayerModelPath(game::Race race, game::Gender gender) const; + + // Phase 1: Resolve texture paths from CharSections.dbc and fill model texture slots. + // Call BEFORE charRenderer->loadModel(). + PlayerTextureInfo resolvePlayerTextures(pipeline::M2Model& model, + game::Race race, game::Gender gender, + uint32_t appearanceBytes); + + // Phase 2: Apply composited textures to loaded model instance. + // Call AFTER charRenderer->loadModel(). Saves skin state for re-compositing. + void compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo); + + // Build default active geosets for player character + std::unordered_set buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId); + + // Equipment weapon loading (reads inventory, attaches weapon M2 models) + void loadEquippedWeapons(); + + // Weapon sheathe state + void setWeaponsSheathed(bool sheathed) { weaponsSheathed_ = sheathed; } + bool isWeaponsSheathed() const { return weaponsSheathed_; } + void toggleWeaponsSheathed() { weaponsSheathed_ = !weaponsSheathed_; } + + // Saved skin state accessors (used by game_screen.cpp for equipment re-compositing) + const std::string& getBodySkinPath() const { return bodySkinPath_; } + const std::vector& getUnderwearPaths() const { return underwearPaths_; } + uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; } + uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; } + +private: + bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel); + + rendering::Renderer* renderer_; + pipeline::AssetManager* assetManager_; + game::GameHandler* gameHandler_; + pipeline::DBCLayout* dbcLayout_; + EntitySpawner* entitySpawner_; + + // Saved at spawn for skin re-compositing on equipment changes + std::string bodySkinPath_; + std::vector underwearPaths_; + uint32_t skinTextureSlotIndex_ = 0; + uint32_t cloakTextureSlotIndex_ = 0; + + bool weaponsSheathed_ = false; +}; + +} // namespace core +} // namespace wowee diff --git a/include/core/application.hpp b/include/core/application.hpp index a3f20984..1865aabf 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -3,6 +3,7 @@ #include "core/window.hpp" #include "core/input.hpp" #include "core/entity_spawner.hpp" +#include "core/appearance_composer.hpp" #include "game/character.hpp" #include "game/game_services.hpp" #include "pipeline/blp_loader.hpp" @@ -73,9 +74,7 @@ public: // Singleton access static Application& getInstance() { return *instance; } - // Weapon loading (called at spawn and on equipment change) - void loadEquippedWeapons(); - bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel); + // Logout to login screen void logoutToLogin(); @@ -85,23 +84,25 @@ public: bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const; bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const; - // Character skin composite state (saved at spawn for re-compositing on equipment change) - const std::string& getBodySkinPath() const { return bodySkinPath_; } - const std::vector& getUnderwearPaths() const { return underwearPaths_; } - uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; } - uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; } + // Character skin composite state — delegated to AppearanceComposer + const std::string& getBodySkinPath() const { return appearanceComposer_ ? appearanceComposer_->getBodySkinPath() : emptyString_; } + const std::vector& getUnderwearPaths() const { return appearanceComposer_ ? appearanceComposer_->getUnderwearPaths() : emptyStringVec_; } + uint32_t getSkinTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getSkinTextureSlotIndex() : 0; } + uint32_t getCloakTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getCloakTextureSlotIndex() : 0; } uint32_t getGryphonDisplayId() const { return entitySpawner_ ? entitySpawner_->getGryphonDisplayId() : 0; } uint32_t getWyvernDisplayId() const { return entitySpawner_ ? entitySpawner_->getWyvernDisplayId() : 0; } // Entity spawner access EntitySpawner* getEntitySpawner() { return entitySpawner_.get(); } + // Appearance composer access + AppearanceComposer* getAppearanceComposer() { return appearanceComposer_.get(); } + private: void update(float deltaTime); void render(); void setupUICallbacks(); void spawnPlayerCharacter(); - std::string getPlayerModelPath() const; static const char* mapIdToName(uint32_t mapId); static const char* mapDisplayName(uint32_t mapId); void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); @@ -123,6 +124,7 @@ private: std::unique_ptr expansionRegistry_; std::unique_ptr dbcLayout_; std::unique_ptr entitySpawner_; + std::unique_ptr appearanceComposer_; AppState state = AppState::AUTHENTICATION; bool running = false; @@ -140,11 +142,9 @@ private: uint32_t spawnedAppearanceBytes_ = 0; uint8_t spawnedFacialFeatures_ = 0; - // Saved at spawn for skin re-compositing - std::string bodySkinPath_; - std::vector underwearPaths_; - uint32_t skinTextureSlotIndex_ = 0; - uint32_t cloakTextureSlotIndex_ = 0; + // Static empty values for null-safe delegation + static inline const std::string emptyString_; + static inline const std::vector emptyStringVec_; bool lastTaxiFlight_ = false; uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none) @@ -174,7 +174,6 @@ private: glm::vec3 chargeEndPos_{0.0f}; // Render coordinates uint64_t chargeTargetGuid_ = 0; - bool weaponsSheathed_ = false; bool wasAutoAttacking_ = false; bool mapNameCacheLoaded_ = false; std::unordered_map mapNameById_; diff --git a/src/core/appearance_composer.cpp b/src/core/appearance_composer.cpp new file mode 100644 index 00000000..cedd2c43 --- /dev/null +++ b/src/core/appearance_composer.cpp @@ -0,0 +1,383 @@ +#include "core/appearance_composer.hpp" +#include "core/entity_spawner.hpp" +#include "core/logger.hpp" +#include "rendering/renderer.hpp" +#include "rendering/character_renderer.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/m2_loader.hpp" +#include "pipeline/dbc_loader.hpp" +#include "pipeline/dbc_layout.hpp" +#include "game/game_handler.hpp" + +namespace wowee { +namespace core { + +AppearanceComposer::AppearanceComposer(rendering::Renderer* renderer, + pipeline::AssetManager* assetManager, + game::GameHandler* gameHandler, + pipeline::DBCLayout* dbcLayout, + EntitySpawner* entitySpawner) + : renderer_(renderer) + , assetManager_(assetManager) + , gameHandler_(gameHandler) + , dbcLayout_(dbcLayout) + , entitySpawner_(entitySpawner) +{ +} + +std::string AppearanceComposer::getPlayerModelPath(game::Race race, game::Gender gender) const { + return game::getPlayerModelPath(race, gender); +} + +PlayerTextureInfo AppearanceComposer::resolvePlayerTextures(pipeline::M2Model& model, + game::Race race, game::Gender gender, + uint32_t appearanceBytes) { + PlayerTextureInfo result; + + uint32_t targetRaceId = static_cast(race); + uint32_t targetSexId = (gender == game::Gender::FEMALE) ? 1u : 0u; + + // Race name for fallback texture paths + const char* raceFolderName = "Human"; + switch (race) { + case game::Race::HUMAN: raceFolderName = "Human"; break; + case game::Race::ORC: raceFolderName = "Orc"; break; + case game::Race::DWARF: raceFolderName = "Dwarf"; break; + case game::Race::NIGHT_ELF: raceFolderName = "NightElf"; break; + case game::Race::UNDEAD: raceFolderName = "Scourge"; break; + case game::Race::TAUREN: raceFolderName = "Tauren"; break; + case game::Race::GNOME: raceFolderName = "Gnome"; break; + case game::Race::TROLL: raceFolderName = "Troll"; break; + case game::Race::BLOOD_ELF: raceFolderName = "BloodElf"; break; + case game::Race::DRAENEI: raceFolderName = "Draenei"; break; + default: break; + } + const char* genderFolder = (gender == game::Gender::FEMALE) ? "Female" : "Male"; + std::string raceGender = std::string(raceFolderName) + genderFolder; + result.bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp"; + std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp"; + + // Extract appearance bytes for texture lookups + uint8_t charSkinId = appearanceBytes & 0xFF; + uint8_t charFaceId = (appearanceBytes >> 8) & 0xFF; + uint8_t charHairStyleId = (appearanceBytes >> 16) & 0xFF; + uint8_t charHairColorId = (appearanceBytes >> 24) & 0xFF; + LOG_INFO("Appearance: skin=", static_cast(charSkinId), " face=", static_cast(charFaceId), + " hairStyle=", static_cast(charHairStyleId), " hairColor=", static_cast(charHairColorId)); + + // Parse CharSections.dbc for skin/face/hair/underwear texture paths + auto charSectionsDbc = assetManager_->loadDBC("CharSections.dbc"); + if (charSectionsDbc) { + LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records"); + const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL); + bool foundSkin = false; + bool foundUnderwear = false; + bool foundFaceLower = false; + bool foundHair = false; + for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { + uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId); + uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId); + uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex); + + if (raceId != targetRaceId || sexId != targetSexId) continue; + + // Section 0 = skin: match by colorIndex = skin byte + if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); + if (!tex1.empty()) { + result.bodySkinPath = tex1; + foundSkin = true; + LOG_INFO(" DBC body skin: ", result.bodySkinPath, " (skin=", static_cast(charSkinId), ")"); + } + } + // Section 3 = hair: match variation=hairStyle, color=hairColor + else if (baseSection == 3 && !foundHair && + variationIndex == charHairStyleId && colorIndex == charHairColorId) { + result.hairTexturePath = charSectionsDbc->getString(r, csF.texture1); + if (!result.hairTexturePath.empty()) { + foundHair = true; + LOG_INFO(" DBC hair texture: ", result.hairTexturePath, + " (style=", static_cast(charHairStyleId), " color=", static_cast(charHairColorId), ")"); + } + } + // Section 1 = face: match variation=faceId, colorIndex=skinId + // Texture1 = face lower, Texture2 = face upper + else if (baseSection == 1 && !foundFaceLower && + variationIndex == charFaceId && colorIndex == charSkinId) { + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); + std::string tex2 = charSectionsDbc->getString(r, csF.texture2); + if (!tex1.empty()) { + result.faceLowerPath = tex1; + LOG_INFO(" DBC face lower: ", result.faceLowerPath); + } + if (!tex2.empty()) { + result.faceUpperPath = tex2; + LOG_INFO(" DBC face upper: ", result.faceUpperPath); + } + foundFaceLower = true; + } + // Section 4 = underwear + else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) { + for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { + std::string tex = charSectionsDbc->getString(r, f); + if (!tex.empty()) { + result.underwearPaths.push_back(tex); + LOG_INFO(" DBC underwear texture: ", tex); + } + } + foundUnderwear = true; + } + + if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break; + } + + if (!foundHair) { + LOG_WARNING("No DBC hair match for style=", static_cast(charHairStyleId), + " color=", static_cast(charHairColorId), + " race=", targetRaceId, " sex=", targetSexId); + } + } else { + LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures"); + } + + // Fill model texture slots with resolved paths + for (auto& tex : model.textures) { + if (tex.type == 1 && tex.filename.empty()) { + tex.filename = result.bodySkinPath; + } else if (tex.type == 6) { + if (!result.hairTexturePath.empty()) { + tex.filename = result.hairTexturePath; + } else if (tex.filename.empty()) { + tex.filename = std::string("Character\\") + raceFolderName + "\\Hair00_00.blp"; + } + } else if (tex.type == 8 && tex.filename.empty()) { + if (!result.underwearPaths.empty()) { + tex.filename = result.underwearPaths[0]; + } else { + tex.filename = pelvisPath; + } + } + } + + return result; +} + +void AppearanceComposer::compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo) { + if (!renderer_) return; + auto* charRenderer = renderer_->getCharacterRenderer(); + if (!charRenderer) return; + + // Save skin composite state for re-compositing on equipment changes + // Include face textures so compositeWithRegions can rebuild the full base + bodySkinPath_ = texInfo.bodySkinPath; + underwearPaths_.clear(); + if (!texInfo.faceLowerPath.empty()) underwearPaths_.push_back(texInfo.faceLowerPath); + if (!texInfo.faceUpperPath.empty()) underwearPaths_.push_back(texInfo.faceUpperPath); + for (const auto& up : texInfo.underwearPaths) underwearPaths_.push_back(up); + + // Composite body skin + face + underwear overlays + { + std::vector layers; + layers.push_back(texInfo.bodySkinPath); + if (!texInfo.faceLowerPath.empty()) layers.push_back(texInfo.faceLowerPath); + if (!texInfo.faceUpperPath.empty()) layers.push_back(texInfo.faceUpperPath); + for (const auto& up : texInfo.underwearPaths) { + layers.push_back(up); + } + if (layers.size() > 1) { + rendering::VkTexture* compositeTex = charRenderer->compositeTextures(layers); + if (compositeTex != 0) { + // Find type-1 (skin) texture slot and replace with composite + // We need model texture info — walk slots via charRenderer + // Use the model slot ID to find the right texture index + auto* modelData = charRenderer->getModelData(modelSlotId); + if (modelData) { + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + if (modelData->textures[ti].type == 1) { + charRenderer->setModelTexture(modelSlotId, static_cast(ti), compositeTex); + skinTextureSlotIndex_ = static_cast(ti); + LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+face+underwear"); + break; + } + } + } + } + } + } + + // Override hair texture on GPU (type-6 slot) after model load + if (!texInfo.hairTexturePath.empty()) { + rendering::VkTexture* hairTex = charRenderer->loadTexture(texInfo.hairTexturePath); + if (hairTex) { + auto* modelData = charRenderer->getModelData(modelSlotId); + if (modelData) { + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + if (modelData->textures[ti].type == 6) { + charRenderer->setModelTexture(modelSlotId, static_cast(ti), hairTex); + LOG_INFO("Applied DBC hair texture to slot ", ti, ": ", texInfo.hairTexturePath); + break; + } + } + } + } + } + + // Find cloak (type-2, Object Skin) texture slot index + { + auto* modelData = charRenderer->getModelData(modelSlotId); + if (modelData) { + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + if (modelData->textures[ti].type == 2) { + cloakTextureSlotIndex_ = static_cast(ti); + LOG_INFO("Cloak texture slot: ", ti); + break; + } + } + } + } +} + +std::unordered_set AppearanceComposer::buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId) { + std::unordered_set activeGeosets; + // Body parts (group 0: IDs 0-99, some models use up to 27) + for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i); + + // Hair style geoset: group 1 = 100 + variation + 1 + activeGeosets.insert(static_cast(100 + hairStyleId + 1)); + // Facial hair geoset: group 2 = 200 + variation + 1 + activeGeosets.insert(static_cast(200 + facialId + 1)); + activeGeosets.insert(kGeosetBareForearms); + activeGeosets.insert(kGeosetBareShins); + activeGeosets.insert(kGeosetDefaultEars); + activeGeosets.insert(kGeosetBareSleeves); + activeGeosets.insert(kGeosetDefaultKneepads); + activeGeosets.insert(kGeosetBarePants); + activeGeosets.insert(kGeosetWithCape); + activeGeosets.insert(kGeosetBareFeet); + // 1703 = DK eye glow mesh — skip for normal characters + // Normal eyes are part of the face texture on the body mesh + return activeGeosets; +} + +bool AppearanceComposer::loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel) { + auto m2Data = assetManager_->readFile(m2Path); + if (m2Data.empty()) return false; + outModel = pipeline::M2Loader::load(m2Data); + // Load skin (WotLK+ M2 format): strip .m2, append 00.skin + std::string skinPath = m2Path; + size_t dotPos = skinPath.rfind('.'); + if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos); + skinPath += "00.skin"; + auto skinData = assetManager_->readFile(skinPath); + if (!skinData.empty() && outModel.version >= 264) + pipeline::M2Loader::loadSkin(skinData, outModel); + return outModel.isValid(); +} + +void AppearanceComposer::loadEquippedWeapons() { + if (!renderer_ || !renderer_->getCharacterRenderer() || !assetManager_ || !assetManager_->isInitialized()) + return; + if (!gameHandler_) return; + + auto* charRenderer = renderer_->getCharacterRenderer(); + uint32_t charInstanceId = renderer_->getCharacterInstanceId(); + if (charInstanceId == 0) return; + + auto& inventory = gameHandler_->getInventory(); + + // Load ItemDisplayInfo.dbc + auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc"); + if (!displayInfoDbc) { + LOG_WARNING("loadEquippedWeapons: failed to load ItemDisplayInfo.dbc"); + return; + } + // Mapping: EquipSlot → attachment ID (1=RightHand, 2=LeftHand) + struct WeaponSlot { + game::EquipSlot slot; + uint32_t attachmentId; + }; + WeaponSlot weaponSlots[] = { + { game::EquipSlot::MAIN_HAND, 1 }, + { game::EquipSlot::OFF_HAND, 2 }, + }; + + if (weaponsSheathed_) { + for (const auto& ws : weaponSlots) { + charRenderer->detachWeapon(charInstanceId, ws.attachmentId); + } + return; + } + + for (const auto& ws : weaponSlots) { + const auto& equipSlot = inventory.getEquipSlot(ws.slot); + + // If slot is empty or has no displayInfoId, detach any existing weapon + if (equipSlot.empty() || equipSlot.item.displayInfoId == 0) { + charRenderer->detachWeapon(charInstanceId, ws.attachmentId); + continue; + } + + uint32_t displayInfoId = equipSlot.item.displayInfoId; + int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId); + if (recIdx < 0) { + LOG_WARNING("loadEquippedWeapons: displayInfoId ", displayInfoId, " not found in DBC"); + charRenderer->detachWeapon(charInstanceId, ws.attachmentId); + continue; + } + + const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + std::string modelName = displayInfoDbc->getString(static_cast(recIdx), idiL ? (*idiL)["LeftModel"] : 1); + std::string textureName = displayInfoDbc->getString(static_cast(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3); + + if (modelName.empty()) { + LOG_WARNING("loadEquippedWeapons: empty model name for displayInfoId ", displayInfoId); + charRenderer->detachWeapon(charInstanceId, ws.attachmentId); + continue; + } + + // Convert .mdx → .m2 + std::string modelFile = modelName; + { + size_t dotPos = modelFile.rfind('.'); + if (dotPos != std::string::npos) { + modelFile = modelFile.substr(0, dotPos) + ".m2"; + } else { + modelFile += ".m2"; + } + } + + // Try Weapon directory first, then Shield + std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile; + pipeline::M2Model weaponModel; + if (!loadWeaponM2(m2Path, weaponModel)) { + m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile; + if (!loadWeaponM2(m2Path, weaponModel)) { + LOG_WARNING("loadEquippedWeapons: failed to load ", modelFile); + charRenderer->detachWeapon(charInstanceId, ws.attachmentId); + continue; + } + } + + // Build texture path + std::string texturePath; + if (!textureName.empty()) { + texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp"; + if (!assetManager_->fileExists(texturePath)) { + texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp"; + } + } + + uint32_t weaponModelId = entitySpawner_->allocateWeaponModelId(); + bool ok = charRenderer->attachWeapon(charInstanceId, ws.attachmentId, + weaponModel, weaponModelId, texturePath); + if (ok) { + LOG_INFO("Equipped weapon: ", m2Path, " at attachment ", ws.attachmentId); + } + } +} + +} // namespace core +} // namespace wowee diff --git a/src/core/application.cpp b/src/core/application.cpp index eb6cee00..433bc288 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -84,19 +84,6 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) { raw[0] == 'n' || raw[0] == 'N'); } -// Default (bare) geoset IDs per equipment group. -// Each group's base is groupNumber * 100; variant 01 is typically bare/default. -constexpr uint16_t kGeosetDefaultConnector = 101; // Group 1: default hair connector -constexpr uint16_t kGeosetBareForearms = 401; // Group 4: no gloves -constexpr uint16_t kGeosetBareShins = 503; // Group 5: no boots -constexpr uint16_t kGeosetDefaultEars = 702; // Group 7: ears -constexpr uint16_t kGeosetBareSleeves = 801; // Group 8: no chest armor sleeves -constexpr uint16_t kGeosetDefaultKneepads = 902; // Group 9: kneepads -constexpr uint16_t kGeosetDefaultTabard = 1201; // Group 12: tabard base -constexpr uint16_t kGeosetBarePants = 1301; // Group 13: no leggings -constexpr uint16_t kGeosetNoCape = 1501; // Group 15: no cape -constexpr uint16_t kGeosetWithCape = 1502; // Group 15: with cape -constexpr uint16_t kGeosetBareFeet = 2002; // Group 20: bare feet } // namespace @@ -204,10 +191,6 @@ const char* Application::mapIdToName(uint32_t mapId) { } } -std::string Application::getPlayerModelPath() const { - return game::getPlayerModelPath(playerRace_, playerGender_); -} - Application* Application::instance = nullptr; @@ -342,6 +325,10 @@ bool Application::initialize() { dbcLayout_.get(), &gameServices_); entitySpawner_->initialize(); + appearanceComposer_ = std::make_unique( + renderer.get(), assetManager.get(), gameHandler.get(), + dbcLayout_.get(), entitySpawner_.get()); + // Ensure the main in-world CharacterRenderer can load textures immediately. // Previously this was only wired during terrain initialization, which meant early spawns // (before terrain load) would render with white fallback textures (notably hair). @@ -970,7 +957,7 @@ void Application::setState(AppState newState) { npcsSpawned = false; playerCharacterSpawned = false; addonsLoaded_ = false; - weaponsSheathed_ = false; + if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); wasAutoAttacking_ = false; loadedMapId_ = 0xFFFFFFFF; spawnedPlayerGuid_ = 0; @@ -1105,7 +1092,7 @@ void Application::logoutToLogin() { // --- Per-session flags --- npcsSpawned = false; playerCharacterSpawned = false; - weaponsSheathed_ = false; + if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); wasAutoAttacking_ = false; loadedMapId_ = 0xFFFFFFFF; lastTaxiFlight_ = false; @@ -1241,9 +1228,9 @@ void Application::update(float deltaTime) { updateCheckpoint = "in_game: auto-unsheathe"; if (gameHandler) { const bool autoAttacking = gameHandler->isAutoAttacking(); - if (autoAttacking && !wasAutoAttacking_ && weaponsSheathed_) { - weaponsSheathed_ = false; - loadEquippedWeapons(); + if (autoAttacking && !wasAutoAttacking_ && appearanceComposer_ && appearanceComposer_->isWeaponsSheathed()) { + appearanceComposer_->setWeaponsSheathed(false); + appearanceComposer_->loadEquippedWeapons(); } wasAutoAttacking_ = autoAttacking; } @@ -1254,9 +1241,9 @@ void Application::update(float deltaTime) { { const bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard; auto& input = Input::getInstance(); - if (!uiWantsKeyboard && input.isKeyJustPressed(SDL_SCANCODE_Z)) { - weaponsSheathed_ = !weaponsSheathed_; - loadEquippedWeapons(); + if (!uiWantsKeyboard && input.isKeyJustPressed(SDL_SCANCODE_Z) && appearanceComposer_) { + appearanceComposer_->toggleWeaponsSheathed(); + appearanceComposer_->loadEquippedWeapons(); } } @@ -3606,7 +3593,7 @@ void Application::spawnPlayerCharacter() { auto* charRenderer = renderer->getCharacterRenderer(); auto* camera = renderer->getCamera(); bool loaded = false; - std::string m2Path = getPlayerModelPath(); + std::string m2Path = appearanceComposer_->getPlayerModelPath(playerRace_, playerGender_); std::string modelDir; std::string baseName; { @@ -3643,144 +3630,18 @@ void Application::spawnPlayerCharacter() { LOG_INFO(" Texture ", ti, ": type=", tex.type, " name='", tex.filename, "'"); } - // Look up textures from CharSections.dbc for all races + // Resolve textures from CharSections.dbc via AppearanceComposer + PlayerTextureInfo texInfo; bool useCharSections = true; - uint32_t targetRaceId = static_cast(playerRace_); - uint32_t targetSexId = (playerGender_ == game::Gender::FEMALE) ? 1u : 0u; - - // Race name for fallback texture paths - const char* raceFolderName = "Human"; - switch (playerRace_) { - case game::Race::HUMAN: raceFolderName = "Human"; break; - case game::Race::ORC: raceFolderName = "Orc"; break; - case game::Race::DWARF: raceFolderName = "Dwarf"; break; - case game::Race::NIGHT_ELF: raceFolderName = "NightElf"; break; - case game::Race::UNDEAD: raceFolderName = "Scourge"; break; - case game::Race::TAUREN: raceFolderName = "Tauren"; break; - case game::Race::GNOME: raceFolderName = "Gnome"; break; - case game::Race::TROLL: raceFolderName = "Troll"; break; - case game::Race::BLOOD_ELF: raceFolderName = "BloodElf"; break; - case game::Race::DRAENEI: raceFolderName = "Draenei"; break; - default: break; - } - const char* genderFolder = (playerGender_ == game::Gender::FEMALE) ? "Female" : "Male"; - std::string raceGender = std::string(raceFolderName) + genderFolder; - std::string bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp"; - std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp"; - std::string faceLowerTexturePath; - std::string faceUpperTexturePath; - std::vector underwearPaths; - - // Extract appearance bytes for texture lookups - uint8_t charSkinId = 0, charFaceId = 0, charHairStyleId = 0, charHairColorId = 0; - if (gameHandler) { - const game::Character* activeChar = gameHandler->getActiveCharacter(); - if (activeChar) { - charSkinId = activeChar->appearanceBytes & 0xFF; - charFaceId = (activeChar->appearanceBytes >> 8) & 0xFF; - charHairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF; - charHairColorId = (activeChar->appearanceBytes >> 24) & 0xFF; - LOG_INFO("Appearance: skin=", static_cast(charSkinId), " face=", static_cast(charFaceId), - " hairStyle=", static_cast(charHairStyleId), " hairColor=", static_cast(charHairColorId)); - } - } - - std::string hairTexturePath; - if (useCharSections) { - auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); - if (charSectionsDbc) { - LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records"); - const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; - auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL); - bool foundSkin = false; - bool foundUnderwear = false; - bool foundFaceLower = false; - bool foundHair = false; - for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId); - uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId); - uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex); - - if (raceId != targetRaceId || sexId != targetSexId) continue; - - // Section 0 = skin: match by colorIndex = skin byte - if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { - std::string tex1 = charSectionsDbc->getString(r, csF.texture1); - if (!tex1.empty()) { - bodySkinPath = tex1; - foundSkin = true; - LOG_INFO(" DBC body skin: ", bodySkinPath, " (skin=", static_cast(charSkinId), ")"); - } - } - // Section 3 = hair: match variation=hairStyle, color=hairColor - else if (baseSection == 3 && !foundHair && - variationIndex == charHairStyleId && colorIndex == charHairColorId) { - hairTexturePath = charSectionsDbc->getString(r, csF.texture1); - if (!hairTexturePath.empty()) { - foundHair = true; - LOG_INFO(" DBC hair texture: ", hairTexturePath, - " (style=", static_cast(charHairStyleId), " color=", static_cast(charHairColorId), ")"); - } - } - // Section 1 = face: match variation=faceId, colorIndex=skinId - // Texture1 = face lower, Texture2 = face upper - else if (baseSection == 1 && !foundFaceLower && - variationIndex == charFaceId && colorIndex == charSkinId) { - std::string tex1 = charSectionsDbc->getString(r, csF.texture1); - std::string tex2 = charSectionsDbc->getString(r, csF.texture2); - if (!tex1.empty()) { - faceLowerTexturePath = tex1; - LOG_INFO(" DBC face lower: ", faceLowerTexturePath); - } - if (!tex2.empty()) { - faceUpperTexturePath = tex2; - LOG_INFO(" DBC face upper: ", faceUpperTexturePath); - } - foundFaceLower = true; - } - // Section 4 = underwear - else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) { - for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { - std::string tex = charSectionsDbc->getString(r, f); - if (!tex.empty()) { - underwearPaths.push_back(tex); - LOG_INFO(" DBC underwear texture: ", tex); - } - } - foundUnderwear = true; - } - - if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break; - } - - if (!foundHair) { - LOG_WARNING("No DBC hair match for style=", static_cast(charHairStyleId), - " color=", static_cast(charHairColorId), - " race=", targetRaceId, " sex=", targetSexId); - } - } else { - LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures"); - } - - for (auto& tex : model.textures) { - if (tex.type == 1 && tex.filename.empty()) { - tex.filename = bodySkinPath; - } else if (tex.type == 6) { - if (!hairTexturePath.empty()) { - tex.filename = hairTexturePath; - } else if (tex.filename.empty()) { - tex.filename = std::string("Character\\") + raceFolderName + "\\Hair00_00.blp"; - } - } else if (tex.type == 8 && tex.filename.empty()) { - if (!underwearPaths.empty()) { - tex.filename = underwearPaths[0]; - } else { - tex.filename = pelvisPath; - } + if (appearanceComposer_) { + uint32_t appearanceBytes = 0; + if (gameHandler) { + const game::Character* activeChar = gameHandler->getActiveCharacter(); + if (activeChar) { + appearanceBytes = activeChar->appearanceBytes; } } + texInfo = appearanceComposer_->resolvePlayerTextures(model, playerRace_, playerGender_, appearanceBytes); } // Load external .anim files for sequences with external data. @@ -3806,62 +3667,9 @@ void Application::spawnPlayerCharacter() { charRenderer->loadModel(model, 1); - if (useCharSections) { - // Save skin composite state for re-compositing on equipment changes - // Include face textures so compositeWithRegions can rebuild the full base - bodySkinPath_ = bodySkinPath; - underwearPaths_.clear(); - if (!faceLowerTexturePath.empty()) underwearPaths_.push_back(faceLowerTexturePath); - if (!faceUpperTexturePath.empty()) underwearPaths_.push_back(faceUpperTexturePath); - for (const auto& up : underwearPaths) underwearPaths_.push_back(up); - - // Composite body skin + face + underwear overlays - { - std::vector layers; - layers.push_back(bodySkinPath); - if (!faceLowerTexturePath.empty()) layers.push_back(faceLowerTexturePath); - if (!faceUpperTexturePath.empty()) layers.push_back(faceUpperTexturePath); - for (const auto& up : underwearPaths) { - layers.push_back(up); - } - if (layers.size() > 1) { - rendering::VkTexture* compositeTex = charRenderer->compositeTextures(layers); - if (compositeTex != 0) { - for (size_t ti = 0; ti < model.textures.size(); ti++) { - if (model.textures[ti].type == 1) { - charRenderer->setModelTexture(1, static_cast(ti), compositeTex); - skinTextureSlotIndex_ = static_cast(ti); - LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+face+underwear"); - break; - } - } - } - } - } - // Override hair texture on GPU (type-6 slot) after model load - if (!hairTexturePath.empty()) { - rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexturePath); - if (hairTex) { - for (size_t ti = 0; ti < model.textures.size(); ti++) { - if (model.textures[ti].type == 6) { - charRenderer->setModelTexture(1, static_cast(ti), hairTex); - LOG_INFO("Applied DBC hair texture to slot ", ti, ": ", hairTexturePath); - break; - } - } - } - } - } else { - bodySkinPath_.clear(); - underwearPaths_.clear(); - } - // Find cloak (type-2, Object Skin) texture slot index - for (size_t ti = 0; ti < model.textures.size(); ti++) { - if (model.textures[ti].type == 2) { - cloakTextureSlotIndex_ = static_cast(ti); - LOG_INFO("Cloak texture slot: ", ti); - break; - } + // Apply composited textures via AppearanceComposer (saves skin state for re-compositing) + if (useCharSections && appearanceComposer_) { + appearanceComposer_->compositePlayerSkin(1, texInfo); } loaded = true; @@ -3942,12 +3750,7 @@ void Application::spawnPlayerCharacter() { renderer->getCharacterPosition() = spawnPos; renderer->setCharacterFollow(instanceId); - // Default geosets for the active character (match CharacterPreview logic). - // Previous hardcoded values (notably always inserting 101) caused wrong hair meshes in-world. - std::unordered_set activeGeosets; - // Body parts (group 0: IDs 0-99, some models use up to 27) - for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i); - + // Build default geosets for the active character via AppearanceComposer uint8_t hairStyleId = 0; uint8_t facialId = 0; if (gameHandler) { @@ -3956,20 +3759,9 @@ void Application::spawnPlayerCharacter() { facialId = ch->facialFeatures; } } - // Hair style geoset: group 1 = 100 + variation + 1 - activeGeosets.insert(static_cast(100 + hairStyleId + 1)); - // Facial hair geoset: group 2 = 200 + variation + 1 - activeGeosets.insert(static_cast(200 + facialId + 1)); - activeGeosets.insert(kGeosetBareForearms); - activeGeosets.insert(kGeosetBareShins); - activeGeosets.insert(kGeosetDefaultEars); - activeGeosets.insert(kGeosetBareSleeves); - activeGeosets.insert(kGeosetDefaultKneepads); - activeGeosets.insert(kGeosetBarePants); - activeGeosets.insert(kGeosetWithCape); - activeGeosets.insert(kGeosetBareFeet); - // 1703 = DK eye glow mesh — skip for normal characters - // Normal eyes are part of the face texture on the body mesh + auto activeGeosets = appearanceComposer_ + ? appearanceComposer_->buildDefaultPlayerGeosets(hairStyleId, facialId) + : std::unordered_set{}; charRenderer->setActiveGeosets(instanceId, activeGeosets); // Play idle animation (Stand = animation ID 0) @@ -4024,124 +3816,7 @@ void Application::spawnPlayerCharacter() { } // Load equipped weapons (sword + shield) - loadEquippedWeapons(); - } -} - -bool Application::loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel) { - auto m2Data = assetManager->readFile(m2Path); - if (m2Data.empty()) return false; - outModel = pipeline::M2Loader::load(m2Data); - // Load skin (WotLK+ M2 format): strip .m2, append 00.skin - std::string skinPath = m2Path; - size_t dotPos = skinPath.rfind('.'); - if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos); - skinPath += "00.skin"; - auto skinData = assetManager->readFile(skinPath); - if (!skinData.empty() && outModel.version >= 264) - pipeline::M2Loader::loadSkin(skinData, outModel); - return outModel.isValid(); -} - -void Application::loadEquippedWeapons() { - if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) - return; - if (!gameHandler) return; - - auto* charRenderer = renderer->getCharacterRenderer(); - uint32_t charInstanceId = renderer->getCharacterInstanceId(); - if (charInstanceId == 0) return; - - auto& inventory = gameHandler->getInventory(); - - // Load ItemDisplayInfo.dbc - auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); - if (!displayInfoDbc) { - LOG_WARNING("loadEquippedWeapons: failed to load ItemDisplayInfo.dbc"); - return; - } - // Mapping: EquipSlot → attachment ID (1=RightHand, 2=LeftHand) - struct WeaponSlot { - game::EquipSlot slot; - uint32_t attachmentId; - }; - WeaponSlot weaponSlots[] = { - { game::EquipSlot::MAIN_HAND, 1 }, - { game::EquipSlot::OFF_HAND, 2 }, - }; - - if (weaponsSheathed_) { - for (const auto& ws : weaponSlots) { - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - } - return; - } - - for (const auto& ws : weaponSlots) { - const auto& equipSlot = inventory.getEquipSlot(ws.slot); - - // If slot is empty or has no displayInfoId, detach any existing weapon - if (equipSlot.empty() || equipSlot.item.displayInfoId == 0) { - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - continue; - } - - uint32_t displayInfoId = equipSlot.item.displayInfoId; - int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId); - if (recIdx < 0) { - LOG_WARNING("loadEquippedWeapons: displayInfoId ", displayInfoId, " not found in DBC"); - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - continue; - } - - const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; - std::string modelName = displayInfoDbc->getString(static_cast(recIdx), idiL ? (*idiL)["LeftModel"] : 1); - std::string textureName = displayInfoDbc->getString(static_cast(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3); - - if (modelName.empty()) { - LOG_WARNING("loadEquippedWeapons: empty model name for displayInfoId ", displayInfoId); - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - continue; - } - - // Convert .mdx → .m2 - std::string modelFile = modelName; - { - size_t dotPos = modelFile.rfind('.'); - if (dotPos != std::string::npos) { - modelFile = modelFile.substr(0, dotPos) + ".m2"; - } else { - modelFile += ".m2"; - } - } - - // Try Weapon directory first, then Shield - std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile; - pipeline::M2Model weaponModel; - if (!loadWeaponM2(m2Path, weaponModel)) { - m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile; - if (!loadWeaponM2(m2Path, weaponModel)) { - LOG_WARNING("loadEquippedWeapons: failed to load ", modelFile); - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - continue; - } - } - - // Build texture path - std::string texturePath; - if (!textureName.empty()) { - texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp"; - if (!assetManager->fileExists(texturePath)) { - texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp"; - } - } - - uint32_t weaponModelId = entitySpawner_->allocateWeaponModelId(); - bool ok = charRenderer->attachWeapon(charInstanceId, ws.attachmentId, - weaponModel, weaponModelId, texturePath); - if (ok) { - LOG_INFO("Equipped weapon: ", m2Path, " at attachment ", ws.attachmentId); - } + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); } } @@ -4514,8 +4189,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float playerGender_ = activeChar->gender; playerClass_ = activeChar->characterClass; spawnSnapToGround = false; - weaponsSheathed_ = false; - loadEquippedWeapons(); // will no-op until instance exists + if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); // will no-op until instance exists spawnPlayerCharacter(); } renderer->getCharacterPosition() = spawnRender; @@ -4568,7 +4243,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Spawn player character now that renderers are initialized if (!playerCharacterSpawned) { spawnPlayerCharacter(); - loadEquippedWeapons(); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); } // Load the root WMO @@ -4810,7 +4485,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // player model now that the renderer actually exists. if (!playerCharacterSpawned) { spawnPlayerCharacter(); - loadEquippedWeapons(); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); } showProgress("Streaming terrain tiles...", 0.35f); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 12937d62..e7a2e271 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -631,7 +631,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) { updateCharacterGeosets(gameHandler.getInventory()); updateCharacterTextures(gameHandler.getInventory()); - core::Application::getInstance().loadEquippedWeapons(); + if (auto* ac = core::Application::getInstance().getAppearanceComposer()) ac->loadEquippedWeapons(); inventoryScreen.markPreviewDirty(); // Update renderer weapon type for animation selection auto* r = core::Application::getInstance().getRenderer(); From 9b38e64f84f28e4a70b4436a87b878fcad492115 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 1 Apr 2026 20:06:26 +0300 Subject: [PATCH 2/4] "Fix and refine app initialization flow - Update core application startup paths and cleanup logic - Adjust renderer & input subsystem integration for stability - Address recent staging source updates with robust error handling" --- CMakeLists.txt | 1 + include/core/application.hpp | 36 +- include/core/world_loader.hpp | 125 ++++ src/core/application.cpp | 1220 ++------------------------------- src/core/world_loader.cpp | 1217 ++++++++++++++++++++++++++++++++ 5 files changed, 1391 insertions(+), 1208 deletions(-) create mode 100644 include/core/world_loader.hpp create mode 100644 src/core/world_loader.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 101a92cf..83fccadd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -488,6 +488,7 @@ set(WOWEE_SOURCES src/core/application.cpp src/core/entity_spawner.cpp src/core/appearance_composer.cpp + src/core/world_loader.cpp src/core/window.cpp src/core/input.cpp src/core/logger.cpp diff --git a/include/core/application.hpp b/include/core/application.hpp index 1865aabf..2f47e489 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -4,6 +4,7 @@ #include "core/input.hpp" #include "core/entity_spawner.hpp" #include "core/appearance_composer.hpp" +#include "core/world_loader.hpp" #include "game/character.hpp" #include "game/game_services.hpp" #include "pipeline/blp_loader.hpp" @@ -43,6 +44,8 @@ enum class AppState { }; class Application { + friend class WorldLoader; + public: Application(); ~Application(); @@ -98,14 +101,14 @@ public: // Appearance composer access AppearanceComposer* getAppearanceComposer() { return appearanceComposer_.get(); } + // World loader access + WorldLoader* getWorldLoader() { return worldLoader_.get(); } + private: void update(float deltaTime); void render(); void setupUICallbacks(); void spawnPlayerCharacter(); - static const char* mapIdToName(uint32_t mapId); - static const char* mapDisplayName(uint32_t mapId); - void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); void buildFactionHostilityMap(uint8_t playerRace); void setupTestTransport(); // Test transport boat for development @@ -125,6 +128,7 @@ private: std::unique_ptr dbcLayout_; std::unique_ptr entitySpawner_; std::unique_ptr appearanceComposer_; + std::unique_ptr worldLoader_; AppState state = AppState::AUTHENTICATION; bool running = false; @@ -147,13 +151,6 @@ private: static inline const std::vector emptyStringVec_; bool lastTaxiFlight_ = false; - uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none) - uint32_t worldLoadGeneration_ = 0; // Incremented on each world entry to detect re-entrant loads - bool loadingWorld_ = false; // True while loadOnlineWorldTerrain is running - struct PendingWorldEntry { - uint32_t mapId; float x, y, z; - }; - std::optional pendingWorldEntry_; // Deferred world entry during loading float taxiLandingClampTimer_ = 0.0f; float worldEntryMovementGraceTimer_ = 0.0f; @@ -175,29 +172,10 @@ private: uint64_t chargeTargetGuid_ = 0; bool wasAutoAttacking_ = false; - bool mapNameCacheLoaded_ = false; - std::unordered_map mapNameById_; // Quest marker billboard sprites (above NPCs) void loadQuestMarkerModels(); // Now loads BLP textures void updateQuestMarkers(); // Updates billboard positions - - // Background world preloader — warms AssetManager file cache for the - // expected world before the user clicks Enter World. - struct WorldPreload { - uint32_t mapId = 0; - std::string mapName; - int centerTileX = 0; - int centerTileY = 0; - std::atomic cancel{false}; - std::vector workers; - }; - std::unique_ptr worldPreload_; - void startWorldPreload(uint32_t mapId, const std::string& mapName, float serverX, float serverY); - void cancelWorldPreload(); - void saveLastWorldInfo(uint32_t mapId, const std::string& mapName, float serverX, float serverY); - struct LastWorldInfo { uint32_t mapId = 0; std::string mapName; float x = 0, y = 0; bool valid = false; }; - LastWorldInfo loadLastWorldInfo() const; }; } // namespace core diff --git a/include/core/world_loader.hpp b/include/core/world_loader.hpp new file mode 100644 index 00000000..01ce5483 --- /dev/null +++ b/include/core/world_loader.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { + +namespace rendering { class Renderer; } +namespace pipeline { class AssetManager; class DBCLayout; } +namespace game { class GameHandler; class World; } +namespace addons { class AddonManager; } + +namespace core { + +class Application; +class EntitySpawner; +class AppearanceComposer; +class Window; + +/// Handles terrain streaming, map transitions, world preloading, +/// and coordinate-aware tile management for online world entry. +class WorldLoader { +public: + WorldLoader(Application& app, + rendering::Renderer* renderer, + pipeline::AssetManager* assetManager, + game::GameHandler* gameHandler, + EntitySpawner* entitySpawner, + AppearanceComposer* appearanceComposer, + Window* window, + game::World* world, + addons::AddonManager* addonManager); + ~WorldLoader(); + + // Main terrain loading — drives loading screen, WMO/ADT detection, player spawn + void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); + + // Process deferred world entry (called from Application::update each frame) + void processPendingEntry(); + + // Map name utilities + static const char* mapIdToName(uint32_t mapId); + static const char* mapDisplayName(uint32_t mapId); + + // Background preloading — warms AssetManager file cache + void startWorldPreload(uint32_t mapId, const std::string& mapName, + float serverX, float serverY); + void cancelWorldPreload(); + + // Persistent world info for session-to-session preloading + void saveLastWorldInfo(uint32_t mapId, const std::string& mapName, + float serverX, float serverY); + struct LastWorldInfo { + uint32_t mapId = 0; + std::string mapName; + float x = 0, y = 0; + bool valid = false; + }; + LastWorldInfo loadLastWorldInfo() const; + + // State accessors + uint32_t getLoadedMapId() const { return loadedMapId_; } + bool isLoadingWorld() const { return loadingWorld_; } + bool hasPendingEntry() const { return pendingWorldEntry_.has_value(); } + + // Get cached map name by ID (returns empty string if not found) + std::string getMapNameById(uint32_t mapId) const { + auto it = mapNameById_.find(mapId); + return (it != mapNameById_.end()) ? it->second : std::string{}; + } + + // Set pending world entry for deferred processing via processPendingEntry() + void setPendingEntry(uint32_t mapId, float x, float y, float z) { + pendingWorldEntry_ = PendingWorldEntry{mapId, x, y, z}; + } + + // Reset methods (for logout / character switch) + void resetLoadedMap() { loadedMapId_ = 0xFFFFFFFF; } + void resetMapNameCache() { mapNameCacheLoaded_ = false; mapNameById_.clear(); } + +private: + Application& app_; + rendering::Renderer* renderer_; + pipeline::AssetManager* assetManager_; + game::GameHandler* gameHandler_; + EntitySpawner* entitySpawner_; + AppearanceComposer* appearanceComposer_; + Window* window_; + game::World* world_; + addons::AddonManager* addonManager_; + + uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none) + uint32_t worldLoadGeneration_ = 0; // Incremented on each world entry to detect re-entrant loads + bool loadingWorld_ = false; // True while loadOnlineWorldTerrain is running + + struct PendingWorldEntry { + uint32_t mapId; float x, y, z; + }; + std::optional pendingWorldEntry_; + + // Map.dbc name cache (loaded once per session) + bool mapNameCacheLoaded_ = false; + std::unordered_map mapNameById_; + + // Background world preloader — warms AssetManager file cache for the + // expected world before the user clicks Enter World. + struct WorldPreload { + uint32_t mapId = 0; + std::string mapName; + int centerTileX = 0; + int centerTileY = 0; + std::atomic cancel{false}; + std::vector workers; + }; + std::unique_ptr worldPreload_; +}; + +} // namespace core +} // namespace wowee diff --git a/src/core/application.cpp b/src/core/application.cpp index 433bc288..a91f8223 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -86,112 +86,6 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) { } // namespace - -const char* Application::mapDisplayName(uint32_t mapId) { - // Friendly display names for the loading screen - switch (mapId) { - case 0: return "Eastern Kingdoms"; - case 1: return "Kalimdor"; - case 530: return "Outland"; - case 571: return "Northrend"; - default: return nullptr; - } -} - -const char* Application::mapIdToName(uint32_t mapId) { - // Fallback when Map.dbc is unavailable. Names must match WDT directory names - // (case-insensitive — AssetManager lowercases all paths). - switch (mapId) { - // Continents - case 0: return "Azeroth"; - case 1: return "Kalimdor"; - case 530: return "Expansion01"; - case 571: return "Northrend"; - // Classic dungeons/raids - case 30: return "PVPZone01"; - case 33: return "Shadowfang"; - case 34: return "StormwindJail"; - case 36: return "DeadminesInstance"; - case 43: return "WailingCaverns"; - case 47: return "RazserfenKraulInstance"; - case 48: return "Blackfathom"; - case 70: return "Uldaman"; - case 90: return "GnomeragonInstance"; - case 109: return "SunkenTemple"; - case 129: return "RazorfenDowns"; - case 189: return "MonasteryInstances"; - case 209: return "TanarisInstance"; - case 229: return "BlackRockSpire"; - case 230: return "BlackrockDepths"; - case 249: return "OnyxiaLairInstance"; - case 289: return "ScholomanceInstance"; - case 309: return "Zul'Gurub"; - case 329: return "Stratholme"; - case 349: return "Mauradon"; - case 369: return "DeeprunTram"; - case 389: return "OrgrimmarInstance"; - case 409: return "MoltenCore"; - case 429: return "DireMaul"; - case 469: return "BlackwingLair"; - case 489: return "PVPZone03"; - case 509: return "AhnQiraj"; - case 529: return "PVPZone04"; - case 531: return "AhnQirajTemple"; - case 533: return "Stratholme Raid"; - // TBC - case 532: return "Karazahn"; - case 534: return "HyjalPast"; - case 540: return "HellfireMilitary"; - case 542: return "HellfireDemon"; - case 543: return "HellfireRampart"; - case 544: return "HellfireRaid"; - case 545: return "CoilfangPumping"; - case 546: return "CoilfangMarsh"; - case 547: return "CoilfangDraenei"; - case 548: return "CoilfangRaid"; - case 550: return "TempestKeepRaid"; - case 552: return "TempestKeepArcane"; - case 553: return "TempestKeepAtrium"; - case 554: return "TempestKeepFactory"; - case 555: return "AuchindounShadow"; - case 556: return "AuchindounDraenei"; - case 557: return "AuchindounEthereal"; - case 558: return "AuchindounDemon"; - case 560: return "HillsbradPast"; - case 564: return "BlackTemple"; - case 565: return "GruulsLair"; - case 566: return "PVPZone05"; - case 568: return "ZulAman"; - case 580: return "SunwellPlateau"; - case 585: return "Sunwell5ManFix"; - // WotLK - case 574: return "Valgarde70"; - case 575: return "UtgardePinnacle"; - case 576: return "Nexus70"; - case 578: return "Nexus80"; - case 595: return "StratholmeCOT"; - case 599: return "Ulduar70"; - case 600: return "Ulduar80"; - case 601: return "DrakTheronKeep"; - case 602: return "GunDrak"; - case 603: return "UlduarRaid"; - case 608: return "DalaranPrison"; - case 615: return "ChamberOfAspectsBlack"; - case 617: return "DeathKnightStart"; - case 619: return "Azjol_Uppercity"; - case 624: return "WintergraspRaid"; - case 631: return "IcecrownCitadel"; - case 632: return "IcecrownCitadel5Man"; - case 649: return "ArgentTournamentRaid"; - case 650: return "ArgentTournamentDungeon"; - case 658: return "QuarryOfTears"; - case 668: return "HallsOfReflection"; - case 724: return "ChamberOfAspectsRed"; - default: return ""; - } -} - - Application* Application::instance = nullptr; Application::Application() { @@ -342,15 +236,6 @@ bool Application::initialize() { gameHandler->getTransportManager()->loadTaxiPathNodeDBC(assetManager.get()); } - // Start background preload for last-played character's world. - // Warms the file cache so terrain tile loading is faster at Enter World. - { - auto lastWorld = loadLastWorldInfo(); - if (lastWorld.valid) { - startWorldPreload(lastWorld.mapId, lastWorld.mapName, lastWorld.x, lastWorld.y); - } - } - // Initialize addon system addonManager_ = std::make_unique(); if (addonManager_->initialize(gameHandler.get())) { @@ -608,6 +493,21 @@ bool Application::initialize() { addonManager_.reset(); } + // Initialize world loader (handles terrain streaming, world preload, map transitions) + worldLoader_ = std::make_unique( + *this, renderer.get(), assetManager.get(), gameHandler.get(), + entitySpawner_.get(), appearanceComposer_.get(), window.get(), + world.get(), addonManager_.get()); + + // Start background preload for last-played character's world. + // Warms the file cache so terrain tile loading is faster at Enter World. + { + auto lastWorld = worldLoader_->loadLastWorldInfo(); + if (lastWorld.valid) { + worldLoader_->startWorldPreload(lastWorld.mapId, lastWorld.mapName, lastWorld.x, lastWorld.y); + } + } + } else { LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable"); LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory"); @@ -884,7 +784,9 @@ void Application::shutdown() { } // Stop background world preloader before destroying AssetManager - cancelWorldPreload(); + if (worldLoader_) { + worldLoader_->cancelWorldPreload(); + }; // Save floor cache before renderer is destroyed if (renderer && renderer->getWMORenderer()) { @@ -959,7 +861,7 @@ void Application::setState(AppState newState) { addonsLoaded_ = false; if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); wasAutoAttacking_ = false; - loadedMapId_ = 0xFFFFFFFF; + if (worldLoader_) worldLoader_->resetLoadedMap(); spawnedPlayerGuid_ = 0; spawnedAppearanceBytes_ = 0; spawnedFacialFeatures_ = 0; @@ -1065,8 +967,7 @@ void Application::reloadExpansionData() { } // Reset map name cache so it reloads from new expansion's Map.dbc - mapNameCacheLoaded_ = false; - mapNameById_.clear(); + if (worldLoader_) worldLoader_->resetMapNameCache(); // Reset game handler DBC caches so they reload from new expansion data if (gameHandler) { @@ -1094,7 +995,7 @@ void Application::logoutToLogin() { playerCharacterSpawned = false; if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); wasAutoAttacking_ = false; - loadedMapId_ = 0xFFFFFFFF; + if (worldLoader_) worldLoader_->resetLoadedMap(); lastTaxiFlight_ = false; taxiLandingClampTimer_ = 0.0f; worldEntryMovementGraceTimer_ = 0.0f; @@ -2129,18 +2030,9 @@ void Application::update(float deltaTime) { break; } - if (pendingWorldEntry_ && !loadingWorld_ && state != AppState::DISCONNECTED) { - auto entry = *pendingWorldEntry_; - pendingWorldEntry_.reset(); - worldEntryMovementGraceTimer_ = 2.0f; - taxiLandingClampTimer_ = 0.0f; - lastTaxiFlight_ = false; - if (renderer && renderer->getCameraController()) { - renderer->getCameraController()->clearMovementInputs(); - renderer->getCameraController()->suppressMovementFor(1.0f); - renderer->getCameraController()->suspendGravityFor(10.0f); - } - loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z); + // Process any pending world entry request via WorldLoader + if (worldLoader_ && state != AppState::DISCONNECTED) { + worldLoader_->processPendingEntry(); } // Update renderer (camera, etc.) only when in-game @@ -2339,7 +2231,8 @@ void Application::setupUICallbacks() { // Reconnect to the same map: terrain stays loaded but all online entities are stale. // Despawn them properly so the server's fresh CREATE_OBJECTs will re-populate the world. - if (entitySpawner_ && mapId == loadedMapId_ && renderer && renderer->getTerrainManager() && isInitialEntry) { + uint32_t currentLoadedMap = worldLoader_ ? worldLoader_->getLoadedMapId() : 0xFFFFFFFF; + if (entitySpawner_ && mapId == currentLoadedMap && renderer && renderer->getTerrainManager() && isInitialEntry) { LOG_INFO("Reconnect to same map ", mapId, ": clearing stale online entities (terrain preserved)"); // Pending spawn queues and failure caches — clear so previously-failed GUIDs can retry. @@ -2379,7 +2272,7 @@ void Application::setupUICallbacks() { } // Same-map teleport (taxi landing, GM teleport, hearthstone on same continent): - if (mapId == loadedMapId_ && renderer && renderer->getTerrainManager()) { + if (mapId == currentLoadedMap && renderer && renderer->getTerrainManager()) { // Check if teleport is far enough to need terrain loading (>500 render units) glm::vec3 oldPos = renderer->getCharacterPosition(); glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(x, y, z)); @@ -2401,7 +2294,7 @@ void Application::setupUICallbacks() { renderer->getCameraController()->suppressMovementFor(1.0f); renderer->getCameraController()->suspendGravityFor(10.0f); } - pendingWorldEntry_ = PendingWorldEntry{mapId, x, y, z}; + if (worldLoader_) worldLoader_->setPendingEntry(mapId, x, y, z); return; } LOG_INFO("Same-map teleport (map ", mapId, "), skipping full world reload"); @@ -2446,9 +2339,9 @@ void Application::setupUICallbacks() { // If a world load is already in progress (re-entrant call from // gameHandler->update() processing SMSG_NEW_WORLD during warmup), // defer this entry. The current load will pick it up when it finishes. - if (loadingWorld_) { + if (worldLoader_ && worldLoader_->isLoadingWorld()) { LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)"); - pendingWorldEntry_ = {mapId, x, y, z}; + worldLoader_->setPendingEntry(mapId, x, y, z); return; } @@ -2457,7 +2350,7 @@ void Application::setupUICallbacks() { // it runs after the current packet handler returns instead of recursing // from `SMSG_LOGIN_VERIFY_WORLD` / `SMSG_NEW_WORLD`. LOG_WARNING("Queued world entry: map ", mapId, " pos=(", x, ", ", y, ", ", z, ")"); - pendingWorldEntry_ = {mapId, x, y, z}; + if (worldLoader_) worldLoader_->setPendingEntry(mapId, x, y, z); }); auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional { @@ -2687,14 +2580,16 @@ void Application::setupUICallbacks() { // Resolve map name from the cached Map.dbc table std::string mapName; - if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) { - mapName = it->second; - } else { - mapName = mapIdToName(mapId); + if (worldLoader_) { + mapName = worldLoader_->getMapNameById(mapId); + } + if (mapName.empty()) { + mapName = WorldLoader::mapIdToName(mapId); } if (mapName.empty()) mapName = "Azeroth"; - if (mapId == loadedMapId_) { + uint32_t currentLoadedMap = worldLoader_ ? worldLoader_->getLoadedMapId() : 0xFFFFFFFF; + if (mapId == currentLoadedMap) { // Same map: pre-enqueue tiles around the bind point so workers start // loading them now. Uses render-space coords (canonicalToRender). // Use radius 4 (9x9=81 tiles) — hearthstone cast is ~10s, enough time @@ -2716,7 +2611,9 @@ void Application::setupUICallbacks() { // loadOnlineWorldTerrain runs its blocking load loop. // homeBindPos_ is canonical; startWorldPreload expects server coords. glm::vec3 server = core::coords::canonicalToServer(glm::vec3(x, y, z)); - startWorldPreload(mapId, mapName, server.x, server.y); + if (worldLoader_) { + worldLoader_->startWorldPreload(mapId, mapName, server.x, server.y); + } LOG_INFO("Hearthstone preload: started file cache warm for map '", mapName, "' (id=", mapId, ")"); } @@ -3948,920 +3845,6 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) { " hostile (friendGroup=0x", std::hex, playerFriendGroup, ", enemyGroup=0x", playerEnemyGroup, std::dec, ")"); } -void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z) { - if (!renderer || !assetManager || !assetManager->isInitialized()) { - LOG_WARNING("Cannot load online terrain: renderer or assets not ready"); - return; - } - - // Guard against re-entrant calls. The worldEntryCallback defers new - // entries while this flag is set; we process them at the end. - loadingWorld_ = true; - pendingWorldEntry_.reset(); - - // --- Loading screen for online mode --- - rendering::LoadingScreen loadingScreen; - loadingScreen.setVkContext(window->getVkContext()); - loadingScreen.setSDLWindow(window->getSDLWindow()); - bool loadingScreenOk = loadingScreen.initialize(); - - auto showProgress = [&](const char* msg, float progress) { - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) { - window->setShouldClose(true); - loadingScreen.shutdown(); - return; - } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_RESIZED) { - int w = event.window.data1; - int h = event.window.data2; - window->setSize(w, h); - // Vulkan viewport set in command buffer - if (renderer && renderer->getCamera()) { - renderer->getCamera()->setAspectRatio(static_cast(w) / h); - } - } - } - if (!loadingScreenOk) return; - loadingScreen.setStatus(msg); - loadingScreen.setProgress(progress); - loadingScreen.render(); - window->swapBuffers(); - }; - - // Set zone name on loading screen — prefer friendly display name, then DBC - { - const char* friendly = mapDisplayName(mapId); - if (friendly) { - loadingScreen.setZoneName(friendly); - } else if (gameHandler) { - std::string dbcName = gameHandler->getMapName(mapId); - if (!dbcName.empty()) - loadingScreen.setZoneName(dbcName); - else - loadingScreen.setZoneName("Loading..."); - } - } - - showProgress("Entering world...", 0.0f); - - // --- Clean up previous map's state on map change --- - // (Same cleanup as logout, but preserves player identity and renderer objects.) - LOG_WARNING("loadOnlineWorldTerrain: mapId=", mapId, " loadedMapId_=", loadedMapId_); - bool hasRendererData = renderer && (renderer->getWMORenderer() || renderer->getM2Renderer()); - if (loadedMapId_ != 0xFFFFFFFF || hasRendererData) { - LOG_WARNING("Map change: cleaning up old map ", loadedMapId_, " before loading map ", mapId); - - // Clear pending queues first (these don't touch GPU resources) - entitySpawner_->clearAllQueues(); - - if (renderer) { - // Clear all world geometry from old map (including textures/models). - // WMO clearAll and M2 clear both call vkDeviceWaitIdle internally, - // ensuring no GPU command buffers reference old resources. - if (auto* wmo = renderer->getWMORenderer()) { - wmo->clearAll(); - } - if (auto* m2 = renderer->getM2Renderer()) { - m2->clear(); - } - - // Full clear of character renderer: removes all instances, models, - // textures, and resets descriptor pools. This prevents stale GPU - // resources from accumulating across map changes (old creature - // models, bone buffers, texture descriptor sets) which can cause - // VK_ERROR_DEVICE_LOST on some drivers. - if (auto* cr = renderer->getCharacterRenderer()) { - cr->clear(); - renderer->setCharacterFollow(0); - } - // Reset equipment dirty tracking so composited textures are rebuilt - // after spawnPlayerCharacter() recreates the character instance. - if (gameHandler) { - gameHandler->resetEquipmentDirtyTracking(); - } - - if (auto* terrain = renderer->getTerrainManager()) { - terrain->softReset(); - terrain->setStreamingEnabled(true); // Re-enable in case previous map disabled it - } - if (auto* questMarkers = renderer->getQuestMarkerRenderer()) { - questMarkers->clear(); - } - renderer->clearMount(); - } - - // Clear application-level instance tracking (after renderer cleanup) - entitySpawner_->resetAllState(); - - // Force player character re-spawn on new map - playerCharacterSpawned = false; - } - - // Resolve map folder name from Map.dbc (authoritative for world/instance maps). - // This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor. - if (!mapNameCacheLoaded_ && assetManager) { - mapNameCacheLoaded_ = true; - if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) { - mapNameById_.reserve(mapDbc->getRecordCount()); - const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr; - for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) { - uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0); - std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1); - if (!internalName.empty() && mapNameById_.find(id) == mapNameById_.end()) { - mapNameById_[id] = std::move(internalName); - } - } - LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById_.size(), " entries"); - } else { - LOG_WARNING("Map.dbc not available; using fallback map-id mapping"); - } - } - - std::string mapName; - if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) { - mapName = it->second; - } else { - mapName = mapIdToName(mapId); - } - if (mapName.empty()) { - LOG_WARNING("Unknown mapId ", mapId, " (no Map.dbc entry); falling back to Azeroth"); - mapName = "Azeroth"; - } - LOG_INFO("Loading online world terrain for map '", mapName, "' (ID ", mapId, ")"); - - // Cancel any stale preload (if it was for a different map, the file cache - // still retains whatever was loaded — it doesn't hurt). - if (worldPreload_) { - if (worldPreload_->mapId == mapId) { - LOG_INFO("World preload: cache-warm hit for map '", mapName, "'"); - } else { - LOG_INFO("World preload: map mismatch (preloaded ", worldPreload_->mapName, - ", entering ", mapName, ")"); - } - } - cancelWorldPreload(); - - // Save this world info for next session's early preload - saveLastWorldInfo(mapId, mapName, x, y); - - // Convert server coordinates to canonical WoW coordinates - // Server sends: X=West (canonical.Y), Y=North (canonical.X), Z=Up - glm::vec3 spawnCanonical = core::coords::serverToCanonical(glm::vec3(x, y, z)); - glm::vec3 spawnRender = core::coords::canonicalToRender(spawnCanonical); - - // Set camera position and facing from server orientation - if (renderer->getCameraController()) { - float yawDeg = 0.0f; - if (gameHandler) { - float canonicalYaw = gameHandler->getMovementInfo().orientation; - yawDeg = 180.0f - glm::degrees(canonicalYaw); - } - renderer->getCameraController()->setOnlineMode(true); - renderer->getCameraController()->setDefaultSpawn(spawnRender, yawDeg, -15.0f); - renderer->getCameraController()->reset(); - } - - // Set map name for WMO renderer and reset instance mode - if (renderer->getWMORenderer()) { - renderer->getWMORenderer()->setMapName(mapName); - renderer->getWMORenderer()->setWMOOnlyMap(false); - } - - // Set map name for terrain manager - if (renderer->getTerrainManager()) { - renderer->getTerrainManager()->setMapName(mapName); - } - - // NOTE: TransportManager renderer connection moved to after initializeRenderers (later in this function) - - // Connect WMORenderer to M2Renderer (for hierarchical transforms: doodads following WMO parents) - if (renderer->getWMORenderer() && renderer->getM2Renderer()) { - renderer->getWMORenderer()->setM2Renderer(renderer->getM2Renderer()); - LOG_INFO("WMORenderer connected to M2Renderer for hierarchical doodad transforms"); - } - - showProgress("Loading character model...", 0.05f); - - // Build faction hostility map for this character's race - if (gameHandler) { - const game::Character* activeChar = gameHandler->getActiveCharacter(); - if (activeChar) { - buildFactionHostilityMap(static_cast(activeChar->race)); - } - } - - // Spawn player model for online mode (skip if already spawned, e.g. teleport) - if (gameHandler) { - const game::Character* activeChar = gameHandler->getActiveCharacter(); - if (activeChar) { - const uint64_t activeGuid = gameHandler->getActiveCharacterGuid(); - const bool appearanceChanged = - (activeGuid != spawnedPlayerGuid_) || - (activeChar->appearanceBytes != spawnedAppearanceBytes_) || - (activeChar->facialFeatures != spawnedFacialFeatures_) || - (activeChar->race != playerRace_) || - (activeChar->gender != playerGender_) || - (activeChar->characterClass != playerClass_); - - if (!playerCharacterSpawned || appearanceChanged) { - if (appearanceChanged) { - LOG_INFO("Respawning player model for new/changed character: guid=0x", - std::hex, activeGuid, std::dec); - } - // Remove old instance so we don't keep stale visuals. - if (renderer && renderer->getCharacterRenderer()) { - uint32_t oldInst = renderer->getCharacterInstanceId(); - if (oldInst > 0) { - renderer->setCharacterFollow(0); - renderer->clearMount(); - renderer->getCharacterRenderer()->removeInstance(oldInst); - } - } - playerCharacterSpawned = false; - spawnedPlayerGuid_ = 0; - spawnedAppearanceBytes_ = 0; - spawnedFacialFeatures_ = 0; - - playerRace_ = activeChar->race; - playerGender_ = activeChar->gender; - playerClass_ = activeChar->characterClass; - spawnSnapToGround = false; - if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); - if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); // will no-op until instance exists - spawnPlayerCharacter(); - } - renderer->getCharacterPosition() = spawnRender; - LOG_INFO("Online player at render pos (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")"); - } else { - LOG_WARNING("No active character found for player model spawning"); - } - } - - showProgress("Loading terrain...", 0.20f); - - // Check WDT to detect WMO-only maps (dungeons, raids, BGs) - bool isWMOOnlyMap = false; - pipeline::WDTInfo wdtInfo; - { - std::string wdtPath = "World\\Maps\\" + mapName + "\\" + mapName + ".wdt"; - LOG_WARNING("Reading WDT: ", wdtPath); - std::vector wdtData = assetManager->readFile(wdtPath); - if (!wdtData.empty()) { - wdtInfo = pipeline::parseWDT(wdtData); - isWMOOnlyMap = wdtInfo.isWMOOnly() && !wdtInfo.rootWMOPath.empty(); - LOG_WARNING("WDT result: isWMOOnly=", isWMOOnlyMap, " rootWMO='", wdtInfo.rootWMOPath, "'"); - } else { - LOG_WARNING("No WDT file found at ", wdtPath); - } - } - - bool terrainOk = false; - - if (isWMOOnlyMap) { - // ---- WMO-only map (dungeon/raid/BG): load root WMO directly ---- - LOG_WARNING("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath); - showProgress("Loading instance geometry...", 0.25f); - - // Initialize renderers if they don't exist yet (first login to a WMO-only map). - // On map change, renderers already exist from the previous map. - if (!renderer->getWMORenderer() || !renderer->getTerrainManager()) { - renderer->initializeRenderers(assetManager.get(), mapName); - } - - // Set map name on WMO renderer and disable terrain streaming (no ADT tiles for instances) - if (renderer->getWMORenderer()) { - renderer->getWMORenderer()->setMapName(mapName); - renderer->getWMORenderer()->setWMOOnlyMap(true); - } - if (renderer->getTerrainManager()) { - renderer->getTerrainManager()->setStreamingEnabled(false); - } - - // Spawn player character now that renderers are initialized - if (!playerCharacterSpawned) { - spawnPlayerCharacter(); - if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); - } - - // Load the root WMO - auto* wmoRenderer = renderer->getWMORenderer(); - LOG_WARNING("WMO-only: wmoRenderer=", (wmoRenderer ? "valid" : "NULL")); - if (wmoRenderer) { - LOG_WARNING("WMO-only: reading root WMO file: ", wdtInfo.rootWMOPath); - std::vector wmoData = assetManager->readFile(wdtInfo.rootWMOPath); - LOG_WARNING("WMO-only: root WMO data size=", wmoData.size()); - if (!wmoData.empty()) { - pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); - LOG_WARNING("WMO-only: parsed WMO model, nGroups=", wmoModel.nGroups); - - if (wmoModel.nGroups > 0) { - showProgress("Loading instance groups...", 0.35f); - std::string basePath = wdtInfo.rootWMOPath; - std::string extension; - if (basePath.size() > 4) { - extension = basePath.substr(basePath.size() - 4); - std::string extLower = extension; - for (char& c : extLower) c = static_cast(std::tolower(static_cast(c))); - if (extLower == ".wmo") { - basePath = basePath.substr(0, basePath.size() - 4); - } - } - - uint32_t loadedGroups = 0; - for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { - char groupSuffix[16]; - snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str()); - std::string groupPath = basePath + groupSuffix; - std::vector groupData = assetManager->readFile(groupPath); - if (groupData.empty()) { - snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); - groupData = assetManager->readFile(basePath + groupSuffix); - } - if (groupData.empty()) { - snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi); - groupData = assetManager->readFile(basePath + groupSuffix); - } - if (!groupData.empty()) { - pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi); - loadedGroups++; - } - - // Update loading progress - if (wmoModel.nGroups > 1) { - float groupProgress = 0.35f + 0.30f * static_cast(gi + 1) / wmoModel.nGroups; - char buf[128]; - snprintf(buf, sizeof(buf), "Loading instance groups... %u / %u", gi + 1, wmoModel.nGroups); - showProgress(buf, groupProgress); - } - } - - LOG_INFO("Loaded ", loadedGroups, " / ", wmoModel.nGroups, " WMO groups for instance"); - } - - // WMO-only maps: MODF uses same format as ADT MODF. - // Apply the same rotation conversion that outdoor WMOs get - // (including the implicit +180° Z yaw), but skip the ZEROPOINT - // position offset for zero-position instances (server sends - // coordinates relative to the WMO, not relative to map corner). - glm::vec3 wmoPos(0.0f); - glm::vec3 wmoRot( - -wdtInfo.rotation[2] * 3.14159f / 180.0f, - -wdtInfo.rotation[0] * 3.14159f / 180.0f, - (wdtInfo.rotation[1] + 180.0f) * 3.14159f / 180.0f - ); - if (wdtInfo.position[0] != 0.0f || wdtInfo.position[1] != 0.0f || wdtInfo.position[2] != 0.0f) { - wmoPos = core::coords::adtToWorld( - wdtInfo.position[0], wdtInfo.position[1], wdtInfo.position[2]); - } - - showProgress("Uploading instance geometry...", 0.70f); - uint32_t wmoModelId = 900000 + mapId; // Unique ID range for instance WMOs - if (wmoRenderer->loadModel(wmoModel, wmoModelId)) { - uint32_t instanceId = wmoRenderer->createInstance(wmoModelId, wmoPos, wmoRot, 1.0f); - if (instanceId > 0) { - LOG_WARNING("Instance WMO loaded: modelId=", wmoModelId, - " instanceId=", instanceId); - LOG_WARNING(" MOHD bbox local: (", - wmoModel.boundingBoxMin.x, ", ", wmoModel.boundingBoxMin.y, ", ", wmoModel.boundingBoxMin.z, - ") to (", wmoModel.boundingBoxMax.x, ", ", wmoModel.boundingBoxMax.y, ", ", wmoModel.boundingBoxMax.z, ")"); - LOG_WARNING(" WMO pos: (", wmoPos.x, ", ", wmoPos.y, ", ", wmoPos.z, - ") rot: (", wmoRot.x, ", ", wmoRot.y, ", ", wmoRot.z, ")"); - LOG_WARNING(" Player render pos: (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")"); - LOG_WARNING(" Player canonical: (", spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")"); - // Show player position in WMO local space - { - glm::mat4 instMat(1.0f); - instMat = glm::translate(instMat, wmoPos); - instMat = glm::rotate(instMat, wmoRot.z, glm::vec3(0,0,1)); - instMat = glm::rotate(instMat, wmoRot.y, glm::vec3(0,1,0)); - instMat = glm::rotate(instMat, wmoRot.x, glm::vec3(1,0,0)); - glm::mat4 invMat = glm::inverse(instMat); - glm::vec3 localPlayer = glm::vec3(invMat * glm::vec4(spawnRender, 1.0f)); - LOG_WARNING(" Player in WMO local: (", localPlayer.x, ", ", localPlayer.y, ", ", localPlayer.z, ")"); - bool inside = localPlayer.x >= wmoModel.boundingBoxMin.x && localPlayer.x <= wmoModel.boundingBoxMax.x && - localPlayer.y >= wmoModel.boundingBoxMin.y && localPlayer.y <= wmoModel.boundingBoxMax.y && - localPlayer.z >= wmoModel.boundingBoxMin.z && localPlayer.z <= wmoModel.boundingBoxMax.z; - LOG_WARNING(" Player inside MOHD bbox: ", inside ? "YES" : "NO"); - } - - // Load doodads from the specified doodad set - auto* m2Renderer = renderer->getM2Renderer(); - if (m2Renderer && !wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { - uint32_t setIdx = std::min(static_cast(wdtInfo.doodadSet), - static_cast(wmoModel.doodadSets.size() - 1)); - const auto& doodadSet = wmoModel.doodadSets[setIdx]; - - showProgress("Loading instance doodads...", 0.75f); - glm::mat4 wmoMatrix(1.0f); - wmoMatrix = glm::translate(wmoMatrix, wmoPos); - wmoMatrix = glm::rotate(wmoMatrix, wmoRot.z, glm::vec3(0, 0, 1)); - wmoMatrix = glm::rotate(wmoMatrix, wmoRot.y, glm::vec3(0, 1, 0)); - wmoMatrix = glm::rotate(wmoMatrix, wmoRot.x, glm::vec3(1, 0, 0)); - - uint32_t loadedDoodads = 0; - for (uint32_t di = 0; di < doodadSet.count; di++) { - uint32_t doodadIdx = doodadSet.startIndex + di; - if (doodadIdx >= wmoModel.doodads.size()) break; - - const auto& doodad = wmoModel.doodads[doodadIdx]; - auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex); - if (nameIt == wmoModel.doodadNames.end()) continue; - - std::string m2Path = nameIt->second; - if (m2Path.empty()) continue; - - if (m2Path.size() > 4) { - std::string ext = m2Path.substr(m2Path.size() - 4); - for (char& c : ext) c = static_cast(std::tolower(static_cast(c))); - if (ext == ".mdx" || ext == ".mdl") { - m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2"; - } - } - - std::vector m2Data = assetManager->readFile(m2Path); - if (m2Data.empty()) continue; - - pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data); - if (m2Model.name.empty()) m2Model.name = m2Path; - - std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; - std::vector skinData = assetManager->readFile(skinPath); - if (!skinData.empty() && m2Model.version >= 264) { - pipeline::M2Loader::loadSkin(skinData, m2Model); - } - if (!m2Model.isValid()) continue; - - glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.x, - doodad.rotation.y, doodad.rotation.z); - glm::mat4 doodadLocal(1.0f); - doodadLocal = glm::translate(doodadLocal, doodad.position); - doodadLocal *= glm::mat4_cast(fixedRotation); - doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale)); - - glm::mat4 worldMatrix = wmoMatrix * doodadLocal; - glm::vec3 worldPos = glm::vec3(worldMatrix[3]); - - uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); - if (!m2Renderer->loadModel(m2Model, doodadModelId)) continue; - uint32_t doodadInstId = m2Renderer->createInstanceWithMatrix(doodadModelId, worldMatrix, worldPos); - if (doodadInstId) m2Renderer->setSkipCollision(doodadInstId, true); - loadedDoodads++; - } - LOG_INFO("Loaded ", loadedDoodads, " instance WMO doodads"); - } - } else { - LOG_WARNING("Failed to create instance WMO instance"); - } - } else { - LOG_WARNING("Failed to load instance WMO model"); - } - } else { - LOG_WARNING("Failed to read root WMO file: ", wdtInfo.rootWMOPath); - } - - // Build collision cache for the instance WMO - showProgress("Building collision cache...", 0.88f); - if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); } - wmoRenderer->loadFloorCache(); - if (wmoRenderer->getFloorCacheSize() == 0) { - showProgress("Computing walkable surfaces...", 0.90f); - if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); } - wmoRenderer->precomputeFloorCache(); - } - } - - // Snap player to WMO floor so they don't fall through on first frame - if (wmoRenderer && renderer) { - glm::vec3 playerPos = renderer->getCharacterPosition(); - // Query floor with generous height margin above spawn point - auto floor = wmoRenderer->getFloorHeight(playerPos.x, playerPos.y, playerPos.z + 50.0f); - if (floor) { - playerPos.z = *floor + 0.1f; // Small offset above floor - renderer->getCharacterPosition() = playerPos; - if (gameHandler) { - glm::vec3 canonical = core::coords::renderToCanonical(playerPos); - gameHandler->setPosition(canonical.x, canonical.y, canonical.z); - } - LOG_INFO("Snapped player to instance WMO floor: z=", *floor); - } else { - LOG_WARNING("Could not find WMO floor at player spawn (", - playerPos.x, ", ", playerPos.y, ", ", playerPos.z, ")"); - } - } - - // Diagnostic: verify WMO renderer state after instance loading - LOG_WARNING("=== INSTANCE WMO LOAD COMPLETE ==="); - LOG_WARNING(" wmoRenderer models loaded: ", wmoRenderer->getLoadedModelCount()); - LOG_WARNING(" wmoRenderer instances: ", wmoRenderer->getInstanceCount()); - LOG_WARNING(" wmoRenderer floor cache: ", wmoRenderer->getFloorCacheSize()); - - terrainOk = true; // Mark as OK so post-load setup runs - } else { - // ---- Normal ADT-based map ---- - // Compute ADT tile from canonical coordinates - auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y); - std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + - std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt"; - LOG_INFO("Loading ADT tile [", tileX, ",", tileY, "] from canonical (", - spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")"); - - // Load the initial terrain tile - terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath); - if (!terrainOk) { - LOG_WARNING("Could not load terrain for online world - atmospheric rendering only"); - } else { - LOG_INFO("Online world terrain loading initiated"); - } - - // Set map name on WMO renderer (initializeRenderers handles terrain/minimap/worldMap) - if (renderer->getWMORenderer()) { - renderer->getWMORenderer()->setMapName(mapName); - } - - // Character renderer is created inside loadTestTerrain(), so spawn the - // player model now that the renderer actually exists. - if (!playerCharacterSpawned) { - spawnPlayerCharacter(); - if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); - } - - showProgress("Streaming terrain tiles...", 0.35f); - - // Wait for surrounding terrain tiles to stream in - if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) { - auto* terrainMgr = renderer->getTerrainManager(); - auto* camera = renderer->getCamera(); - - // Use a small radius for the initial load (just immediate tiles), - // then restore the full radius after entering the game. - // This matches WoW's behavior: load quickly, stream the rest in-game. - const int savedLoadRadius = 4; - terrainMgr->setLoadRadius(3); // 7x7=49 tiles — prevents hitches on spawn - terrainMgr->setUnloadRadius(7); - - // Trigger tile streaming for surrounding area - terrainMgr->update(*camera, 1.0f); - - auto startTime = std::chrono::high_resolution_clock::now(); - auto lastProgressTime = startTime; - const float maxWaitSeconds = 60.0f; - const float stallSeconds = 10.0f; - int initialRemaining = terrainMgr->getRemainingTileCount(); - if (initialRemaining < 1) initialRemaining = 1; - int lastRemaining = initialRemaining; - - // Wait until all pending + ready-queue tiles are finalized - while (terrainMgr->getRemainingTileCount() > 0) { - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) { - window->setShouldClose(true); - loadingScreen.shutdown(); - return; - } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_RESIZED) { - int w = event.window.data1; - int h = event.window.data2; - window->setSize(w, h); - // Vulkan viewport set in command buffer - if (renderer->getCamera()) { - renderer->getCamera()->setAspectRatio(static_cast(w) / h); - } - } - } - - // Trigger new streaming — enqueue tiles for background workers - terrainMgr->update(*camera, 0.016f); - - // Process ONE tile per iteration so the progress bar updates - // smoothly between tiles instead of stalling on large batches. - terrainMgr->processOneReadyTile(); - - int remaining = terrainMgr->getRemainingTileCount(); - int loaded = terrainMgr->getLoadedTileCount(); - int total = loaded + remaining; - if (total < 1) total = 1; - float tileProgress = static_cast(loaded) / static_cast(total); - float progress = 0.35f + tileProgress * 0.50f; - - auto now = std::chrono::high_resolution_clock::now(); - float elapsedSec = std::chrono::duration(now - startTime).count(); - - char buf[192]; - if (loaded > 0 && remaining > 0) { - float tilesPerSec = static_cast(loaded) / std::max(elapsedSec, 0.1f); - float etaSec = static_cast(remaining) / std::max(tilesPerSec, 0.1f); - snprintf(buf, sizeof(buf), "Loading terrain... %d / %d tiles (%.0f tiles/s, ~%.0fs remaining)", - loaded, total, tilesPerSec, etaSec); - } else { - snprintf(buf, sizeof(buf), "Loading terrain... %d / %d tiles", - loaded, total); - } - - if (loadingScreenOk) { - loadingScreen.setStatus(buf); - loadingScreen.setProgress(progress); - loadingScreen.render(); - window->swapBuffers(); - } - - if (remaining != lastRemaining) { - lastRemaining = remaining; - lastProgressTime = now; - } - - auto elapsed = std::chrono::high_resolution_clock::now() - startTime; - if (std::chrono::duration(elapsed).count() > maxWaitSeconds) { - LOG_WARNING("Online terrain streaming timeout after ", maxWaitSeconds, "s"); - break; - } - auto stalledFor = std::chrono::high_resolution_clock::now() - lastProgressTime; - if (std::chrono::duration(stalledFor).count() > stallSeconds) { - LOG_WARNING("Online terrain streaming stalled for ", stallSeconds, - "s (remaining=", lastRemaining, "), continuing without full preload"); - break; - } - - // Don't sleep if there are more tiles to finalize — keep processing - if (remaining > 0 && terrainMgr->getReadyQueueCount() == 0) { - SDL_Delay(16); - } - } - - LOG_INFO("Online terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded"); - - // Restore full load radius — remaining tiles stream in-game - terrainMgr->setLoadRadius(savedLoadRadius); - - // Load/precompute collision cache - if (renderer->getWMORenderer()) { - showProgress("Building collision cache...", 0.88f); - if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); } - renderer->getWMORenderer()->loadFloorCache(); - if (renderer->getWMORenderer()->getFloorCacheSize() == 0) { - showProgress("Computing walkable surfaces...", 0.90f); - if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); } - renderer->getWMORenderer()->precomputeFloorCache(); - } - } - } - } - - // Snap player to loaded terrain so they don't spawn underground - if (renderer->getCameraController()) { - renderer->getCameraController()->reset(); - } - - // Test transport disabled — real transports come from server via UPDATEFLAG_TRANSPORT - showProgress("Finalizing world...", 0.94f); - // setupTestTransport(); - - // Connect TransportManager to renderers (must happen AFTER initializeRenderers) - if (gameHandler && gameHandler->getTransportManager()) { - auto* tm = gameHandler->getTransportManager(); - if (renderer->getWMORenderer()) tm->setWMORenderer(renderer->getWMORenderer()); - if (renderer->getM2Renderer()) tm->setM2Renderer(renderer->getM2Renderer()); - LOG_WARNING("TransportManager connected: wmoR=", (renderer->getWMORenderer() ? "yes" : "NULL"), - " m2R=", (renderer->getM2Renderer() ? "yes" : "NULL")); - } - - // Set up NPC animation callbacks (for online creatures) - showProgress("Preparing creatures...", 0.97f); - if (gameHandler && renderer && renderer->getCharacterRenderer()) { - auto* cr = renderer->getCharacterRenderer(); - auto* app = this; - - gameHandler->setNpcDeathCallback([cr, app](uint64_t guid) { - app->entitySpawner_->markCreatureDead(guid); - uint32_t instanceId = app->entitySpawner_->getCreatureInstanceId(guid); - if (instanceId == 0) instanceId = app->entitySpawner_->getPlayerInstanceId(guid); - if (instanceId != 0 && cr) { - cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death - } - }); - - gameHandler->setNpcRespawnCallback([cr, app](uint64_t guid) { - app->entitySpawner_->unmarkCreatureDead(guid); - uint32_t instanceId = app->entitySpawner_->getCreatureInstanceId(guid); - if (instanceId == 0) instanceId = app->entitySpawner_->getPlayerInstanceId(guid); - if (instanceId != 0 && cr) { - cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle - } - }); - - gameHandler->setNpcSwingCallback([cr, app](uint64_t guid) { - uint32_t instanceId = app->entitySpawner_->getCreatureInstanceId(guid); - if (instanceId == 0) instanceId = app->entitySpawner_->getPlayerInstanceId(guid); - if (instanceId != 0 && cr) { - cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1 - } - }); - } - - // Keep the loading screen visible until all spawn/equipment/gameobject queues - // are fully drained. This ensures the player sees a fully populated world - // (character clothed, NPCs placed, game objects loaded) when the screen drops. - { - const float kMinWarmupSeconds = 2.0f; // minimum time to drain network packets - const float kMaxWarmupSeconds = 25.0f; // hard cap to avoid infinite stall - const auto warmupStart = std::chrono::high_resolution_clock::now(); - // Track consecutive idle iterations (all queues empty) to detect convergence - int idleIterations = 0; - const int kIdleThreshold = 5; // require 5 consecutive empty loops (~80ms) - - while (true) { - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) { - window->setShouldClose(true); - if (loadingScreenOk) loadingScreen.shutdown(); - return; - } - if (event.type == SDL_WINDOWEVENT && - event.window.event == SDL_WINDOWEVENT_RESIZED) { - int w = event.window.data1; - int h = event.window.data2; - window->setSize(w, h); - if (renderer && renderer->getCamera()) { - renderer->getCamera()->setAspectRatio(static_cast(w) / h); - } - } - } - - // Drain network and process deferred spawn/composite queues while hidden. - if (gameHandler) gameHandler->update(1.0f / 60.0f); - - // If a new world entry was deferred during packet processing, - // stop warming up this map — we'll load the new one after cleanup. - if (pendingWorldEntry_) { - LOG_WARNING("loadOnlineWorldTerrain(map ", mapId, - ") — deferred world entry pending, stopping warmup"); - break; - } - - if (world) world->update(1.0f / 60.0f); - - // Process all spawn/equipment/transport queues during warmup - entitySpawner_->update(); - if (auto* cr = renderer ? renderer->getCharacterRenderer() : nullptr) { - cr->processPendingNormalMaps(4); - } - updateQuestMarkers(); - - // Update renderer (terrain streaming, animations) - if (renderer) { - renderer->update(1.0f / 60.0f); - } - - const auto now = std::chrono::high_resolution_clock::now(); - const float elapsed = std::chrono::duration(now - warmupStart).count(); - - // Check if all queues are drained - bool queuesEmpty = !entitySpawner_->hasWorkPending(); - - if (queuesEmpty) { - idleIterations++; - } else { - idleIterations = 0; - } - - // Don't exit warmup until the ground under the player exists. - // In cities like Stormwind, players stand on WMO floors, not terrain. - // Check BOTH terrain AND WMO floor — require at least one to be valid. - bool groundReady = false; - if (renderer) { - glm::vec3 renderSpawn = core::coords::canonicalToRender( - glm::vec3(x, y, z)); - float rx = renderSpawn.x, ry = renderSpawn.y, rz = renderSpawn.z; - - // Check WMO floor FIRST (cities like Stormwind stand on WMO floors). - // Terrain exists below WMOs but at the wrong height. - if (auto* wmo = renderer->getWMORenderer()) { - auto wmoH = wmo->getFloorHeight(rx, ry, rz + 5.0f); - if (wmoH.has_value() && std::abs(*wmoH - rz) < 15.0f) { - groundReady = true; - } - } - // Check terrain — but only if it's close to spawn Z (within 15 units). - // Terrain far below a WMO city doesn't count as ground. - if (!groundReady) { - if (auto* tm = renderer->getTerrainManager()) { - auto tH = tm->getHeightAt(rx, ry); - if (tH.has_value() && std::abs(*tH - rz) < 15.0f) { - groundReady = true; - } - } - } - // After 5s with enough tiles loaded, accept terrain as ready even if - // the height sample doesn't match spawn Z exactly. This handles cases - // where getHeightAt returns a slightly different value than the server's - // spawn Z (e.g. terrain LOD, MCNK chunk boundaries, or spawn inside a - // building where floor height differs from terrain below). - if (!groundReady && elapsed >= 5.0f) { - if (auto* tm = renderer->getTerrainManager()) { - if (tm->getLoadedTileCount() >= 4) { - groundReady = true; - LOG_WARNING("Warmup: using tile-count fallback (", tm->getLoadedTileCount(), " tiles) after ", elapsed, "s"); - } - } - } - - if (!groundReady && elapsed > 5.0f && static_cast(elapsed * 2) % 3 == 0) { - LOG_WARNING("Warmup: ground not ready at spawn (", rx, ",", ry, ",", rz, - ") after ", elapsed, "s"); - } - } - - // Exit when: (min time passed AND queues drained AND ground ready) OR hard cap - bool readyToExit = (elapsed >= kMinWarmupSeconds && idleIterations >= kIdleThreshold && groundReady); - if (readyToExit || elapsed >= kMaxWarmupSeconds) { - if (elapsed >= kMaxWarmupSeconds && !groundReady) { - LOG_WARNING("Warmup hit hard cap (", kMaxWarmupSeconds, "s), ground NOT ready — may fall through world"); - } else if (elapsed >= kMaxWarmupSeconds) { - LOG_WARNING("Warmup hit hard cap (", kMaxWarmupSeconds, "s), entering world with pending work"); - } - break; - } - - const float t = std::clamp(elapsed / kMaxWarmupSeconds, 0.0f, 1.0f); - showProgress("Finalizing world sync...", 0.97f + t * 0.025f); - SDL_Delay(16); - } - } - - // Start intro pan right before entering gameplay so it's visible after loading. - if (renderer->getCameraController()) { - renderer->getCameraController()->startIntroPan(2.8f, 140.0f); - } - - showProgress("Entering world...", 1.0f); - - // Ensure all GPU resources (textures, buffers, pipelines) created during - // world load are fully flushed before the first render frame. Without this, - // vkCmdBeginRenderPass can crash on NVIDIA 590.x when resources from async - // uploads haven't completed their queue operations. - if (renderer && renderer->getVkContext()) { - vkDeviceWaitIdle(renderer->getVkContext()->getDevice()); - } - - if (loadingScreenOk) { - loadingScreen.shutdown(); - } - - // Track which map we actually loaded (used by same-map teleport check). - loadedMapId_ = mapId; - - // Clear loading flag and process any deferred world entry. - // A deferred entry occurs when SMSG_NEW_WORLD arrived during our warmup - // (e.g., an area trigger in a dungeon immediately teleporting the player out). - loadingWorld_ = false; - if (pendingWorldEntry_) { - auto entry = *pendingWorldEntry_; - pendingWorldEntry_.reset(); - LOG_WARNING("Processing deferred world entry: map ", entry.mapId); - worldEntryMovementGraceTimer_ = 2.0f; - taxiLandingClampTimer_ = 0.0f; - lastTaxiFlight_ = false; - // Recursive call — sets loadedMapId_ and IN_GAME state for the final map. - loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z); - return; // The recursive call handles setState(IN_GAME). - } - - // Only enter IN_GAME when this is the final map (no deferred entry pending). - setState(AppState::IN_GAME); - - // Load addons once per session on first world entry - if (addonManager_ && !addonsLoaded_) { - // Set character name for per-character SavedVariables - if (gameHandler) { - const std::string& charName = gameHandler->lookupName(gameHandler->getPlayerGuid()); - if (!charName.empty()) { - addonManager_->setCharacterName(charName); - } else { - // Fallback: find name from character list - for (const auto& c : gameHandler->getCharacters()) { - if (c.guid == gameHandler->getPlayerGuid()) { - addonManager_->setCharacterName(c.name); - break; - } - } - } - } - addonManager_->loadAllAddons(); - addonsLoaded_ = true; - addonManager_->fireEvent("VARIABLES_LOADED"); - addonManager_->fireEvent("PLAYER_LOGIN"); - addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); - } else if (addonManager_ && addonsLoaded_) { - // Subsequent world entries (e.g. teleport, instance entry) - addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); - } -} - // Render bounds/position queries — delegates to EntitySpawner bool Application::getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const { if (entitySpawner_) return entitySpawner_->getRenderBoundsForGuid(guid, outCenter, outRadius); @@ -5126,126 +4109,5 @@ void Application::setupTestTransport() { LOG_INFO("========================================"); } -// ─── World Preloader ───────────────────────────────────────────────────────── -// Pre-warms AssetManager file cache with ADT files (and their _obj0 variants) -// for tiles around the expected spawn position. Runs in background so that -// when loadOnlineWorldTerrain eventually asks TerrainManager workers to parse -// the same files, every readFile() is an instant cache hit instead of disk I/O. - -void Application::startWorldPreload(uint32_t mapId, const std::string& mapName, - float serverX, float serverY) { - cancelWorldPreload(); - if (!assetManager || !assetManager->isInitialized() || mapName.empty()) return; - - glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, 0.0f)); - auto [tileX, tileY] = core::coords::canonicalToTile(canonical.x, canonical.y); - - worldPreload_ = std::make_unique(); - worldPreload_->mapId = mapId; - worldPreload_->mapName = mapName; - worldPreload_->centerTileX = tileX; - worldPreload_->centerTileY = tileY; - - LOG_INFO("World preload: starting for map '", mapName, "' tile [", tileX, ",", tileY, "]"); - - // Build list of tiles to preload (radius 1 = 3x3 = 9 tiles, matching load screen) - struct TileJob { int x, y; }; - auto jobs = std::make_shared>(); - // Center tile first (most important) - jobs->push_back({tileX, tileY}); - for (int dx = -1; dx <= 1; dx++) { - for (int dy = -1; dy <= 1; dy++) { - if (dx == 0 && dy == 0) continue; - int tx = tileX + dx, ty = tileY + dy; - if (tx < 0 || tx > 63 || ty < 0 || ty > 63) continue; - jobs->push_back({tx, ty}); - } - } - - // Spawn worker threads (one per tile for maximum parallelism) - auto cancelFlag = &worldPreload_->cancel; - auto* am = assetManager.get(); - std::string mn = mapName; - - int numWorkers = std::min(static_cast(jobs->size()), 4); - auto nextJob = std::make_shared>(0); - - for (int w = 0; w < numWorkers; w++) { - worldPreload_->workers.emplace_back([am, mn, jobs, nextJob, cancelFlag]() { - while (!cancelFlag->load(std::memory_order_relaxed)) { - int idx = nextJob->fetch_add(1, std::memory_order_relaxed); - if (idx >= static_cast(jobs->size())) break; - - int tx = (*jobs)[idx].x; - int ty = (*jobs)[idx].y; - - // Read ADT file (warms file cache) - std::string adtPath = "World\\Maps\\" + mn + "\\" + mn + "_" + - std::to_string(tx) + "_" + std::to_string(ty) + ".adt"; - am->readFile(adtPath); - if (cancelFlag->load(std::memory_order_relaxed)) break; - - // Read obj0 variant - std::string objPath = "World\\Maps\\" + mn + "\\" + mn + "_" + - std::to_string(tx) + "_" + std::to_string(ty) + "_obj0.adt"; - am->readFile(objPath); - } - LOG_DEBUG("World preload worker finished"); - }); - } -} - -void Application::cancelWorldPreload() { - if (!worldPreload_) return; - worldPreload_->cancel.store(true, std::memory_order_relaxed); - for (auto& t : worldPreload_->workers) { - if (t.joinable()) t.join(); - } - LOG_INFO("World preload: cancelled (map=", worldPreload_->mapName, - " tile=[", worldPreload_->centerTileX, ",", worldPreload_->centerTileY, "])"); - worldPreload_.reset(); -} - -void Application::saveLastWorldInfo(uint32_t mapId, const std::string& mapName, - float serverX, float serverY) { -#ifdef _WIN32 - const char* base = std::getenv("APPDATA"); - std::string dir = base ? std::string(base) + "\\wowee" : "."; -#else - const char* home = std::getenv("HOME"); - std::string dir = home ? std::string(home) + "/.wowee" : "."; -#endif - std::filesystem::create_directories(dir); - std::ofstream f(dir + "/last_world.cfg"); - if (f) { - f << mapId << "\n" << mapName << "\n" << serverX << "\n" << serverY << "\n"; - } -} - -Application::LastWorldInfo Application::loadLastWorldInfo() const { -#ifdef _WIN32 - const char* base = std::getenv("APPDATA"); - std::string dir = base ? std::string(base) + "\\wowee" : "."; -#else - const char* home = std::getenv("HOME"); - std::string dir = home ? std::string(home) + "/.wowee" : "."; -#endif - LastWorldInfo info; - std::ifstream f(dir + "/last_world.cfg"); - if (!f) return info; - std::string line; - try { - if (std::getline(f, line)) info.mapId = static_cast(std::stoul(line)); - if (std::getline(f, line)) info.mapName = line; - if (std::getline(f, line)) info.x = std::stof(line); - if (std::getline(f, line)) info.y = std::stof(line); - } catch (...) { - LOG_WARNING("Malformed last_world.cfg, ignoring saved position"); - return info; - } - info.valid = !info.mapName.empty(); - return info; -} - } // namespace core } // namespace wowee diff --git a/src/core/world_loader.cpp b/src/core/world_loader.cpp new file mode 100644 index 00000000..9e90e747 --- /dev/null +++ b/src/core/world_loader.cpp @@ -0,0 +1,1217 @@ +// WorldLoader — terrain streaming, map transitions, world preloading +// Extracted from Application as part of god-class decomposition (Section 3.3) + +#include "core/world_loader.hpp" +#include "core/application.hpp" +#include "core/entity_spawner.hpp" +#include "core/appearance_composer.hpp" +#include "core/window.hpp" +#include "core/coordinates.hpp" +#include "core/logger.hpp" +#include "rendering/renderer.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/camera.hpp" +#include "rendering/camera_controller.hpp" +#include "rendering/terrain_manager.hpp" +#include "rendering/character_renderer.hpp" +#include "rendering/wmo_renderer.hpp" +#include "rendering/m2_renderer.hpp" +#include "rendering/quest_marker_renderer.hpp" +#include "rendering/loading_screen.hpp" +#include "addons/addon_manager.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_layout.hpp" +#include "pipeline/m2_loader.hpp" +#include "pipeline/wmo_loader.hpp" +#include "pipeline/wdt_loader.hpp" +#include "game/game_handler.hpp" +#include "game/transport_manager.hpp" +#include "game/world.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace core { + +WorldLoader::WorldLoader(Application& app, + rendering::Renderer* renderer, + pipeline::AssetManager* assetManager, + game::GameHandler* gameHandler, + EntitySpawner* entitySpawner, + AppearanceComposer* appearanceComposer, + Window* window, + game::World* world, + addons::AddonManager* addonManager) + : app_(app) + , renderer_(renderer) + , assetManager_(assetManager) + , gameHandler_(gameHandler) + , entitySpawner_(entitySpawner) + , appearanceComposer_(appearanceComposer) + , window_(window) + , world_(world) + , addonManager_(addonManager) +{} + +WorldLoader::~WorldLoader() { + cancelWorldPreload(); +} + +const char* WorldLoader::mapDisplayName(uint32_t mapId) { + // Friendly display names for the loading screen + switch (mapId) { + case 0: return "Eastern Kingdoms"; + case 1: return "Kalimdor"; + case 530: return "Outland"; + case 571: return "Northrend"; + default: return nullptr; + } +} + +const char* WorldLoader::mapIdToName(uint32_t mapId) { + // Fallback when Map.dbc is unavailable. Names must match WDT directory names + // (case-insensitive — AssetManager lowercases all paths). + switch (mapId) { + // Continents + case 0: return "Azeroth"; + case 1: return "Kalimdor"; + case 530: return "Expansion01"; + case 571: return "Northrend"; + // Classic dungeons/raids + case 30: return "PVPZone01"; + case 33: return "Shadowfang"; + case 34: return "StormwindJail"; + case 36: return "DeadminesInstance"; + case 43: return "WailingCaverns"; + case 47: return "RazserfenKraulInstance"; + case 48: return "Blackfathom"; + case 70: return "Uldaman"; + case 90: return "GnomeragonInstance"; + case 109: return "SunkenTemple"; + case 129: return "RazorfenDowns"; + case 189: return "MonasteryInstances"; + case 209: return "TanarisInstance"; + case 229: return "BlackRockSpire"; + case 230: return "BlackrockDepths"; + case 249: return "OnyxiaLairInstance"; + case 289: return "ScholomanceInstance"; + case 309: return "Zul'Gurub"; + case 329: return "Stratholme"; + case 349: return "Mauradon"; + case 369: return "DeeprunTram"; + case 389: return "OrgrimmarInstance"; + case 409: return "MoltenCore"; + case 429: return "DireMaul"; + case 469: return "BlackwingLair"; + case 489: return "PVPZone03"; + case 509: return "AhnQiraj"; + case 529: return "PVPZone04"; + case 531: return "AhnQirajTemple"; + case 533: return "Stratholme Raid"; + // TBC + case 532: return "Karazahn"; + case 534: return "HyjalPast"; + case 540: return "HellfireMilitary"; + case 542: return "HellfireDemon"; + case 543: return "HellfireRampart"; + case 544: return "HellfireRaid"; + case 545: return "CoilfangPumping"; + case 546: return "CoilfangMarsh"; + case 547: return "CoilfangDraenei"; + case 548: return "CoilfangRaid"; + case 550: return "TempestKeepRaid"; + case 552: return "TempestKeepArcane"; + case 553: return "TempestKeepAtrium"; + case 554: return "TempestKeepFactory"; + case 555: return "AuchindounShadow"; + case 556: return "AuchindounDraenei"; + case 557: return "AuchindounEthereal"; + case 558: return "AuchindounDemon"; + case 560: return "HillsbradPast"; + case 564: return "BlackTemple"; + case 565: return "GruulsLair"; + case 566: return "PVPZone05"; + case 568: return "ZulAman"; + case 580: return "SunwellPlateau"; + case 585: return "Sunwell5ManFix"; + // WotLK + case 574: return "Valgarde70"; + case 575: return "UtgardePinnacle"; + case 576: return "Nexus70"; + case 578: return "Nexus80"; + case 595: return "StratholmeCOT"; + case 599: return "Ulduar70"; + case 600: return "Ulduar80"; + case 601: return "DrakTheronKeep"; + case 602: return "GunDrak"; + case 603: return "UlduarRaid"; + case 608: return "DalaranPrison"; + case 615: return "ChamberOfAspectsBlack"; + case 617: return "DeathKnightStart"; + case 619: return "Azjol_Uppercity"; + case 624: return "WintergraspRaid"; + case 631: return "IcecrownCitadel"; + case 632: return "IcecrownCitadel5Man"; + case 649: return "ArgentTournamentRaid"; + case 650: return "ArgentTournamentDungeon"; + case 658: return "QuarryOfTears"; + case 668: return "HallsOfReflection"; + case 724: return "ChamberOfAspectsRed"; + default: return ""; + } +} + +void WorldLoader::processPendingEntry() { + if (!pendingWorldEntry_ || loadingWorld_) return; + auto entry = *pendingWorldEntry_; + pendingWorldEntry_.reset(); + LOG_WARNING("Processing deferred world entry: map ", entry.mapId); + app_.worldEntryMovementGraceTimer_ = 2.0f; + app_.taxiLandingClampTimer_ = 0.0f; + app_.lastTaxiFlight_ = false; + // Clear camera movement inputs before loading terrain + if (renderer_ && renderer_->getCameraController()) { + renderer_->getCameraController()->clearMovementInputs(); + renderer_->getCameraController()->suppressMovementFor(1.0f); + renderer_->getCameraController()->suspendGravityFor(10.0f); + } + loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z); +} + +void WorldLoader::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z) { + if (!renderer_ || !assetManager_ || !assetManager_->isInitialized()) { + LOG_WARNING("Cannot load online terrain: renderer or assets not ready"); + return; + } + + // Guard against re-entrant calls. The worldEntryCallback defers new + // entries while this flag is set; we process them at the end. + loadingWorld_ = true; + pendingWorldEntry_.reset(); + + // --- Loading screen for online mode --- + rendering::LoadingScreen loadingScreen; + loadingScreen.setVkContext(window_->getVkContext()); + loadingScreen.setSDLWindow(window_->getSDLWindow()); + bool loadingScreenOk = loadingScreen.initialize(); + + auto showProgress = [&](const char* msg, float progress) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + window_->setShouldClose(true); + loadingScreen.shutdown(); + return; + } + if (event.type == SDL_WINDOWEVENT && + event.window.event == SDL_WINDOWEVENT_RESIZED) { + int w = event.window.data1; + int h = event.window.data2; + window_->setSize(w, h); + // Vulkan viewport set in command buffer + if (renderer_ && renderer_->getCamera()) { + renderer_->getCamera()->setAspectRatio(static_cast(w) / h); + } + } + } + if (!loadingScreenOk) return; + loadingScreen.setStatus(msg); + loadingScreen.setProgress(progress); + loadingScreen.render(); + window_->swapBuffers(); + }; + + // Set zone name on loading screen — prefer friendly display name, then DBC + { + const char* friendly = mapDisplayName(mapId); + if (friendly) { + loadingScreen.setZoneName(friendly); + } else if (gameHandler_) { + std::string dbcName = gameHandler_->getMapName(mapId); + if (!dbcName.empty()) + loadingScreen.setZoneName(dbcName); + else + loadingScreen.setZoneName("Loading..."); + } + } + + showProgress("Entering world...", 0.0f); + + // --- Clean up previous map's state on map change --- + // (Same cleanup as logout, but preserves player identity and renderer objects.) + LOG_WARNING("loadOnlineWorldTerrain: mapId=", mapId, " loadedMapId_=", loadedMapId_); + bool hasRendererData = renderer_ && (renderer_->getWMORenderer() || renderer_->getM2Renderer()); + if (loadedMapId_ != 0xFFFFFFFF || hasRendererData) { + LOG_WARNING("Map change: cleaning up old map ", loadedMapId_, " before loading map ", mapId); + + // Clear pending queues first (these don't touch GPU resources) + entitySpawner_->clearAllQueues(); + + if (renderer_) { + // Clear all world geometry from old map (including textures/models). + // WMO clearAll and M2 clear both call vkDeviceWaitIdle internally, + // ensuring no GPU command buffers reference old resources. + if (auto* wmo = renderer_->getWMORenderer()) { + wmo->clearAll(); + } + if (auto* m2 = renderer_->getM2Renderer()) { + m2->clear(); + } + + // Full clear of character renderer: removes all instances, models, + // textures, and resets descriptor pools. This prevents stale GPU + // resources from accumulating across map changes (old creature + // models, bone buffers, texture descriptor sets) which can cause + // VK_ERROR_DEVICE_LOST on some drivers. + if (auto* cr = renderer_->getCharacterRenderer()) { + cr->clear(); + renderer_->setCharacterFollow(0); + } + // Reset equipment dirty tracking so composited textures are rebuilt + // after spawnPlayerCharacter() recreates the character instance. + if (gameHandler_) { + gameHandler_->resetEquipmentDirtyTracking(); + } + + if (auto* terrain = renderer_->getTerrainManager()) { + terrain->softReset(); + terrain->setStreamingEnabled(true); // Re-enable in case previous map disabled it + } + if (auto* questMarkers = renderer_->getQuestMarkerRenderer()) { + questMarkers->clear(); + } + renderer_->clearMount(); + } + + // Clear application-level instance tracking (after renderer cleanup) + entitySpawner_->resetAllState(); + + // Force player character re-spawn on new map + app_.playerCharacterSpawned = false; + } + + // Resolve map folder name from Map.dbc (authoritative for world/instance maps). + // This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor. + if (!mapNameCacheLoaded_ && assetManager_) { + mapNameCacheLoaded_ = true; + if (auto mapDbc = assetManager_->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) { + mapNameById_.reserve(mapDbc->getRecordCount()); + const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr; + for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) { + uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0); + std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1); + if (!internalName.empty() && mapNameById_.find(id) == mapNameById_.end()) { + mapNameById_[id] = std::move(internalName); + } + } + LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById_.size(), " entries"); + } else { + LOG_WARNING("Map.dbc not available; using fallback map-id mapping"); + } + } + + std::string mapName; + if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) { + mapName = it->second; + } else { + mapName = mapIdToName(mapId); + } + if (mapName.empty()) { + LOG_WARNING("Unknown mapId ", mapId, " (no Map.dbc entry); falling back to Azeroth"); + mapName = "Azeroth"; + } + LOG_INFO("Loading online world terrain for map '", mapName, "' (ID ", mapId, ")"); + + // Cancel any stale preload (if it was for a different map, the file cache + // still retains whatever was loaded — it doesn't hurt). + if (worldPreload_) { + if (worldPreload_->mapId == mapId) { + LOG_INFO("World preload: cache-warm hit for map '", mapName, "'"); + } else { + LOG_INFO("World preload: map mismatch (preloaded ", worldPreload_->mapName, + ", entering ", mapName, ")"); + } + } + cancelWorldPreload(); + + // Save this world info for next session's early preload + saveLastWorldInfo(mapId, mapName, x, y); + + // Convert server coordinates to canonical WoW coordinates + // Server sends: X=West (canonical.Y), Y=North (canonical.X), Z=Up + glm::vec3 spawnCanonical = core::coords::serverToCanonical(glm::vec3(x, y, z)); + glm::vec3 spawnRender = core::coords::canonicalToRender(spawnCanonical); + + // Set camera position and facing from server orientation + if (renderer_->getCameraController()) { + float yawDeg = 0.0f; + if (gameHandler_) { + float canonicalYaw = gameHandler_->getMovementInfo().orientation; + yawDeg = 180.0f - glm::degrees(canonicalYaw); + } + renderer_->getCameraController()->setOnlineMode(true); + renderer_->getCameraController()->setDefaultSpawn(spawnRender, yawDeg, -15.0f); + renderer_->getCameraController()->reset(); + } + + // Set map name for WMO renderer and reset instance mode + if (renderer_->getWMORenderer()) { + renderer_->getWMORenderer()->setMapName(mapName); + renderer_->getWMORenderer()->setWMOOnlyMap(false); + } + + // Set map name for terrain manager + if (renderer_->getTerrainManager()) { + renderer_->getTerrainManager()->setMapName(mapName); + } + + // NOTE: TransportManager renderer connection moved to after initializeRenderers (later in this function) + + // Connect WMORenderer to M2Renderer (for hierarchical transforms: doodads following WMO parents) + if (renderer_->getWMORenderer() && renderer_->getM2Renderer()) { + renderer_->getWMORenderer()->setM2Renderer(renderer_->getM2Renderer()); + LOG_INFO("WMORenderer connected to M2Renderer for hierarchical doodad transforms"); + } + + showProgress("Loading character model...", 0.05f); + + // Build faction hostility map for this character's race + if (gameHandler_) { + const game::Character* activeChar = gameHandler_->getActiveCharacter(); + if (activeChar) { + app_.buildFactionHostilityMap(static_cast(activeChar->race)); + } + } + + // Spawn player model for online mode (skip if already spawned, e.g. teleport) + if (gameHandler_) { + const game::Character* activeChar = gameHandler_->getActiveCharacter(); + if (activeChar) { + const uint64_t activeGuid = gameHandler_->getActiveCharacterGuid(); + const bool appearanceChanged = + (activeGuid != app_.spawnedPlayerGuid_) || + (activeChar->appearanceBytes != app_.spawnedAppearanceBytes_) || + (activeChar->facialFeatures != app_.spawnedFacialFeatures_) || + (activeChar->race != app_.playerRace_) || + (activeChar->gender != app_.playerGender_) || + (activeChar->characterClass != app_.playerClass_); + + if (!app_.playerCharacterSpawned || appearanceChanged) { + if (appearanceChanged) { + LOG_INFO("Respawning player model for new/changed character: guid=0x", + std::hex, activeGuid, std::dec); + } + // Remove old instance so we don't keep stale visuals. + if (renderer_ && renderer_->getCharacterRenderer()) { + uint32_t oldInst = renderer_->getCharacterInstanceId(); + if (oldInst > 0) { + renderer_->setCharacterFollow(0); + renderer_->clearMount(); + renderer_->getCharacterRenderer()->removeInstance(oldInst); + } + } + app_.playerCharacterSpawned = false; + app_.spawnedPlayerGuid_ = 0; + app_.spawnedAppearanceBytes_ = 0; + app_.spawnedFacialFeatures_ = 0; + + app_.playerRace_ = activeChar->race; + app_.playerGender_ = activeChar->gender; + app_.playerClass_ = activeChar->characterClass; + app_.spawnSnapToGround = false; + if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); // will no-op until instance exists + app_.spawnPlayerCharacter(); + } + renderer_->getCharacterPosition() = spawnRender; + LOG_INFO("Online player at render pos (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")"); + } else { + LOG_WARNING("No active character found for player model spawning"); + } + } + + showProgress("Loading terrain...", 0.20f); + + // Check WDT to detect WMO-only maps (dungeons, raids, BGs) + bool isWMOOnlyMap = false; + pipeline::WDTInfo wdtInfo; + { + std::string wdtPath = "World\\Maps\\" + mapName + "\\" + mapName + ".wdt"; + LOG_WARNING("Reading WDT: ", wdtPath); + std::vector wdtData = assetManager_->readFile(wdtPath); + if (!wdtData.empty()) { + wdtInfo = pipeline::parseWDT(wdtData); + isWMOOnlyMap = wdtInfo.isWMOOnly() && !wdtInfo.rootWMOPath.empty(); + LOG_WARNING("WDT result: isWMOOnly=", isWMOOnlyMap, " rootWMO='", wdtInfo.rootWMOPath, "'"); + } else { + LOG_WARNING("No WDT file found at ", wdtPath); + } + } + + bool terrainOk = false; + + if (isWMOOnlyMap) { + // ---- WMO-only map (dungeon/raid/BG): load root WMO directly ---- + LOG_WARNING("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath); + showProgress("Loading instance geometry...", 0.25f); + + // Initialize renderers if they don't exist yet (first login to a WMO-only map). + // On map change, renderers already exist from the previous map. + if (!renderer_->getWMORenderer() || !renderer_->getTerrainManager()) { + renderer_->initializeRenderers(assetManager_, mapName); + } + + // Set map name on WMO renderer and disable terrain streaming (no ADT tiles for instances) + if (renderer_->getWMORenderer()) { + renderer_->getWMORenderer()->setMapName(mapName); + renderer_->getWMORenderer()->setWMOOnlyMap(true); + } + if (renderer_->getTerrainManager()) { + renderer_->getTerrainManager()->setStreamingEnabled(false); + } + + // Spawn player character now that renderers are initialized + if (!app_.playerCharacterSpawned) { + app_.spawnPlayerCharacter(); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); + } + + // Load the root WMO + auto* wmoRenderer = renderer_->getWMORenderer(); + LOG_WARNING("WMO-only: wmoRenderer=", (wmoRenderer ? "valid" : "NULL")); + if (wmoRenderer) { + LOG_WARNING("WMO-only: reading root WMO file: ", wdtInfo.rootWMOPath); + std::vector wmoData = assetManager_->readFile(wdtInfo.rootWMOPath); + LOG_WARNING("WMO-only: root WMO data size=", wmoData.size()); + if (!wmoData.empty()) { + pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); + LOG_WARNING("WMO-only: parsed WMO model, nGroups=", wmoModel.nGroups); + + if (wmoModel.nGroups > 0) { + showProgress("Loading instance groups...", 0.35f); + std::string basePath = wdtInfo.rootWMOPath; + std::string extension; + if (basePath.size() > 4) { + extension = basePath.substr(basePath.size() - 4); + std::string extLower = extension; + for (char& c : extLower) c = static_cast(std::tolower(static_cast(c))); + if (extLower == ".wmo") { + basePath = basePath.substr(0, basePath.size() - 4); + } + } + + uint32_t loadedGroups = 0; + for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { + char groupSuffix[16]; + snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str()); + std::string groupPath = basePath + groupSuffix; + std::vector groupData = assetManager_->readFile(groupPath); + if (groupData.empty()) { + snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); + groupData = assetManager_->readFile(basePath + groupSuffix); + } + if (groupData.empty()) { + snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi); + groupData = assetManager_->readFile(basePath + groupSuffix); + } + if (!groupData.empty()) { + pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi); + loadedGroups++; + } + + // Update loading progress + if (wmoModel.nGroups > 1) { + float groupProgress = 0.35f + 0.30f * static_cast(gi + 1) / wmoModel.nGroups; + char buf[128]; + snprintf(buf, sizeof(buf), "Loading instance groups... %u / %u", gi + 1, wmoModel.nGroups); + showProgress(buf, groupProgress); + } + } + + LOG_INFO("Loaded ", loadedGroups, " / ", wmoModel.nGroups, " WMO groups for instance"); + } + + // WMO-only maps: MODF uses same format as ADT MODF. + // Apply the same rotation conversion that outdoor WMOs get + // (including the implicit +180° Z yaw), but skip the ZEROPOINT + // position offset for zero-position instances (server sends + // coordinates relative to the WMO, not relative to map corner). + glm::vec3 wmoPos(0.0f); + glm::vec3 wmoRot( + -wdtInfo.rotation[2] * 3.14159f / 180.0f, + -wdtInfo.rotation[0] * 3.14159f / 180.0f, + (wdtInfo.rotation[1] + 180.0f) * 3.14159f / 180.0f + ); + if (wdtInfo.position[0] != 0.0f || wdtInfo.position[1] != 0.0f || wdtInfo.position[2] != 0.0f) { + wmoPos = core::coords::adtToWorld( + wdtInfo.position[0], wdtInfo.position[1], wdtInfo.position[2]); + } + + showProgress("Uploading instance geometry...", 0.70f); + uint32_t wmoModelId = 900000 + mapId; // Unique ID range for instance WMOs + if (wmoRenderer->loadModel(wmoModel, wmoModelId)) { + uint32_t instanceId = wmoRenderer->createInstance(wmoModelId, wmoPos, wmoRot, 1.0f); + if (instanceId > 0) { + LOG_WARNING("Instance WMO loaded: modelId=", wmoModelId, + " instanceId=", instanceId); + LOG_WARNING(" MOHD bbox local: (", + wmoModel.boundingBoxMin.x, ", ", wmoModel.boundingBoxMin.y, ", ", wmoModel.boundingBoxMin.z, + ") to (", wmoModel.boundingBoxMax.x, ", ", wmoModel.boundingBoxMax.y, ", ", wmoModel.boundingBoxMax.z, ")"); + LOG_WARNING(" WMO pos: (", wmoPos.x, ", ", wmoPos.y, ", ", wmoPos.z, + ") rot: (", wmoRot.x, ", ", wmoRot.y, ", ", wmoRot.z, ")"); + LOG_WARNING(" Player render pos: (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")"); + LOG_WARNING(" Player canonical: (", spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")"); + // Show player position in WMO local space + { + glm::mat4 instMat(1.0f); + instMat = glm::translate(instMat, wmoPos); + instMat = glm::rotate(instMat, wmoRot.z, glm::vec3(0,0,1)); + instMat = glm::rotate(instMat, wmoRot.y, glm::vec3(0,1,0)); + instMat = glm::rotate(instMat, wmoRot.x, glm::vec3(1,0,0)); + glm::mat4 invMat = glm::inverse(instMat); + glm::vec3 localPlayer = glm::vec3(invMat * glm::vec4(spawnRender, 1.0f)); + LOG_WARNING(" Player in WMO local: (", localPlayer.x, ", ", localPlayer.y, ", ", localPlayer.z, ")"); + bool inside = localPlayer.x >= wmoModel.boundingBoxMin.x && localPlayer.x <= wmoModel.boundingBoxMax.x && + localPlayer.y >= wmoModel.boundingBoxMin.y && localPlayer.y <= wmoModel.boundingBoxMax.y && + localPlayer.z >= wmoModel.boundingBoxMin.z && localPlayer.z <= wmoModel.boundingBoxMax.z; + LOG_WARNING(" Player inside MOHD bbox: ", inside ? "YES" : "NO"); + } + + // Load doodads from the specified doodad set + auto* m2Renderer = renderer_->getM2Renderer(); + if (m2Renderer && !wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { + uint32_t setIdx = std::min(static_cast(wdtInfo.doodadSet), + static_cast(wmoModel.doodadSets.size() - 1)); + const auto& doodadSet = wmoModel.doodadSets[setIdx]; + + showProgress("Loading instance doodads...", 0.75f); + glm::mat4 wmoMatrix(1.0f); + wmoMatrix = glm::translate(wmoMatrix, wmoPos); + wmoMatrix = glm::rotate(wmoMatrix, wmoRot.z, glm::vec3(0, 0, 1)); + wmoMatrix = glm::rotate(wmoMatrix, wmoRot.y, glm::vec3(0, 1, 0)); + wmoMatrix = glm::rotate(wmoMatrix, wmoRot.x, glm::vec3(1, 0, 0)); + + uint32_t loadedDoodads = 0; + for (uint32_t di = 0; di < doodadSet.count; di++) { + uint32_t doodadIdx = doodadSet.startIndex + di; + if (doodadIdx >= wmoModel.doodads.size()) break; + + const auto& doodad = wmoModel.doodads[doodadIdx]; + auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex); + if (nameIt == wmoModel.doodadNames.end()) continue; + + std::string m2Path = nameIt->second; + if (m2Path.empty()) continue; + + if (m2Path.size() > 4) { + std::string ext = m2Path.substr(m2Path.size() - 4); + for (char& c : ext) c = static_cast(std::tolower(static_cast(c))); + if (ext == ".mdx" || ext == ".mdl") { + m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2"; + } + } + + std::vector m2Data = assetManager_->readFile(m2Path); + if (m2Data.empty()) continue; + + pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data); + if (m2Model.name.empty()) m2Model.name = m2Path; + + std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; + std::vector skinData = assetManager_->readFile(skinPath); + if (!skinData.empty() && m2Model.version >= 264) { + pipeline::M2Loader::loadSkin(skinData, m2Model); + } + if (!m2Model.isValid()) continue; + + glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.x, + doodad.rotation.y, doodad.rotation.z); + glm::mat4 doodadLocal(1.0f); + doodadLocal = glm::translate(doodadLocal, doodad.position); + doodadLocal *= glm::mat4_cast(fixedRotation); + doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale)); + + glm::mat4 worldMatrix = wmoMatrix * doodadLocal; + glm::vec3 worldPos = glm::vec3(worldMatrix[3]); + + uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); + if (!m2Renderer->loadModel(m2Model, doodadModelId)) continue; + uint32_t doodadInstId = m2Renderer->createInstanceWithMatrix(doodadModelId, worldMatrix, worldPos); + if (doodadInstId) m2Renderer->setSkipCollision(doodadInstId, true); + loadedDoodads++; + } + LOG_INFO("Loaded ", loadedDoodads, " instance WMO doodads"); + } + } else { + LOG_WARNING("Failed to create instance WMO instance"); + } + } else { + LOG_WARNING("Failed to load instance WMO model"); + } + } else { + LOG_WARNING("Failed to read root WMO file: ", wdtInfo.rootWMOPath); + } + + // Build collision cache for the instance WMO + showProgress("Building collision cache...", 0.88f); + if (loadingScreenOk) { loadingScreen.render(); window_->swapBuffers(); } + wmoRenderer->loadFloorCache(); + if (wmoRenderer->getFloorCacheSize() == 0) { + showProgress("Computing walkable surfaces...", 0.90f); + if (loadingScreenOk) { loadingScreen.render(); window_->swapBuffers(); } + wmoRenderer->precomputeFloorCache(); + } + } + + // Snap player to WMO floor so they don't fall through on first frame + if (wmoRenderer && renderer_) { + glm::vec3 playerPos = renderer_->getCharacterPosition(); + // Query floor with generous height margin above spawn point + auto floor = wmoRenderer->getFloorHeight(playerPos.x, playerPos.y, playerPos.z + 50.0f); + if (floor) { + playerPos.z = *floor + 0.1f; // Small offset above floor + renderer_->getCharacterPosition() = playerPos; + if (gameHandler_) { + glm::vec3 canonical = core::coords::renderToCanonical(playerPos); + gameHandler_->setPosition(canonical.x, canonical.y, canonical.z); + } + LOG_INFO("Snapped player to instance WMO floor: z=", *floor); + } else { + LOG_WARNING("Could not find WMO floor at player spawn (", + playerPos.x, ", ", playerPos.y, ", ", playerPos.z, ")"); + } + } + + // Diagnostic: verify WMO renderer state after instance loading + LOG_WARNING("=== INSTANCE WMO LOAD COMPLETE ==="); + LOG_WARNING(" wmoRenderer models loaded: ", wmoRenderer->getLoadedModelCount()); + LOG_WARNING(" wmoRenderer instances: ", wmoRenderer->getInstanceCount()); + LOG_WARNING(" wmoRenderer floor cache: ", wmoRenderer->getFloorCacheSize()); + + terrainOk = true; // Mark as OK so post-load setup runs + } else { + // ---- Normal ADT-based map ---- + // Compute ADT tile from canonical coordinates + auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y); + std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + + std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt"; + LOG_INFO("Loading ADT tile [", tileX, ",", tileY, "] from canonical (", + spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")"); + + // Load the initial terrain tile + terrainOk = renderer_->loadTestTerrain(assetManager_, adtPath); + if (!terrainOk) { + LOG_WARNING("Could not load terrain for online world - atmospheric rendering only"); + } else { + LOG_INFO("Online world terrain loading initiated"); + } + + // Set map name on WMO renderer (initializeRenderers handles terrain/minimap/worldMap) + if (renderer_->getWMORenderer()) { + renderer_->getWMORenderer()->setMapName(mapName); + } + + // Character renderer is created inside loadTestTerrain(), so spawn the + // player model now that the renderer actually exists. + if (!app_.playerCharacterSpawned) { + app_.spawnPlayerCharacter(); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); + } + + showProgress("Streaming terrain tiles...", 0.35f); + + // Wait for surrounding terrain tiles to stream in + if (terrainOk && renderer_->getTerrainManager() && renderer_->getCamera()) { + auto* terrainMgr = renderer_->getTerrainManager(); + auto* camera = renderer_->getCamera(); + + // Use a small radius for the initial load (just immediate tiles), + // then restore the full radius after entering the game. + // This matches WoW's behavior: load quickly, stream the rest in-game. + const int savedLoadRadius = 4; + terrainMgr->setLoadRadius(3); // 7x7=49 tiles — prevents hitches on spawn + terrainMgr->setUnloadRadius(7); + + // Trigger tile streaming for surrounding area + terrainMgr->update(*camera, 1.0f); + + auto startTime = std::chrono::high_resolution_clock::now(); + auto lastProgressTime = startTime; + const float maxWaitSeconds = 60.0f; + const float stallSeconds = 10.0f; + int initialRemaining = terrainMgr->getRemainingTileCount(); + if (initialRemaining < 1) initialRemaining = 1; + int lastRemaining = initialRemaining; + + // Wait until all pending + ready-queue tiles are finalized + while (terrainMgr->getRemainingTileCount() > 0) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + window_->setShouldClose(true); + loadingScreen.shutdown(); + return; + } + if (event.type == SDL_WINDOWEVENT && + event.window.event == SDL_WINDOWEVENT_RESIZED) { + int w = event.window.data1; + int h = event.window.data2; + window_->setSize(w, h); + // Vulkan viewport set in command buffer + if (renderer_->getCamera()) { + renderer_->getCamera()->setAspectRatio(static_cast(w) / h); + } + } + } + + // Trigger new streaming — enqueue tiles for background workers + terrainMgr->update(*camera, 0.016f); + + // Process ONE tile per iteration so the progress bar updates + // smoothly between tiles instead of stalling on large batches. + terrainMgr->processOneReadyTile(); + + int remaining = terrainMgr->getRemainingTileCount(); + int loaded = terrainMgr->getLoadedTileCount(); + int total = loaded + remaining; + if (total < 1) total = 1; + float tileProgress = static_cast(loaded) / static_cast(total); + float progress = 0.35f + tileProgress * 0.50f; + + auto now = std::chrono::high_resolution_clock::now(); + float elapsedSec = std::chrono::duration(now - startTime).count(); + + char buf[192]; + if (loaded > 0 && remaining > 0) { + float tilesPerSec = static_cast(loaded) / std::max(elapsedSec, 0.1f); + float etaSec = static_cast(remaining) / std::max(tilesPerSec, 0.1f); + snprintf(buf, sizeof(buf), "Loading terrain... %d / %d tiles (%.0f tiles/s, ~%.0fs remaining)", + loaded, total, tilesPerSec, etaSec); + } else { + snprintf(buf, sizeof(buf), "Loading terrain... %d / %d tiles", + loaded, total); + } + + if (loadingScreenOk) { + loadingScreen.setStatus(buf); + loadingScreen.setProgress(progress); + loadingScreen.render(); + window_->swapBuffers(); + } + + if (remaining != lastRemaining) { + lastRemaining = remaining; + lastProgressTime = now; + } + + auto elapsed = std::chrono::high_resolution_clock::now() - startTime; + if (std::chrono::duration(elapsed).count() > maxWaitSeconds) { + LOG_WARNING("Online terrain streaming timeout after ", maxWaitSeconds, "s"); + break; + } + auto stalledFor = std::chrono::high_resolution_clock::now() - lastProgressTime; + if (std::chrono::duration(stalledFor).count() > stallSeconds) { + LOG_WARNING("Online terrain streaming stalled for ", stallSeconds, + "s (remaining=", lastRemaining, "), continuing without full preload"); + break; + } + + // Don't sleep if there are more tiles to finalize — keep processing + if (remaining > 0 && terrainMgr->getReadyQueueCount() == 0) { + SDL_Delay(16); + } + } + + LOG_INFO("Online terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded"); + + // Restore full load radius — remaining tiles stream in-game + terrainMgr->setLoadRadius(savedLoadRadius); + + // Load/precompute collision cache + if (renderer_->getWMORenderer()) { + showProgress("Building collision cache...", 0.88f); + if (loadingScreenOk) { loadingScreen.render(); window_->swapBuffers(); } + renderer_->getWMORenderer()->loadFloorCache(); + if (renderer_->getWMORenderer()->getFloorCacheSize() == 0) { + showProgress("Computing walkable surfaces...", 0.90f); + if (loadingScreenOk) { loadingScreen.render(); window_->swapBuffers(); } + renderer_->getWMORenderer()->precomputeFloorCache(); + } + } + } + } + + // Snap player to loaded terrain so they don't spawn underground + if (renderer_->getCameraController()) { + renderer_->getCameraController()->reset(); + } + + // Test transport disabled — real transports come from server via UPDATEFLAG_TRANSPORT + showProgress("Finalizing world...", 0.94f); + // setupTestTransport(); + + // Connect TransportManager to renderers (must happen AFTER initializeRenderers) + if (gameHandler_ && gameHandler_->getTransportManager()) { + auto* tm = gameHandler_->getTransportManager(); + if (renderer_->getWMORenderer()) tm->setWMORenderer(renderer_->getWMORenderer()); + if (renderer_->getM2Renderer()) tm->setM2Renderer(renderer_->getM2Renderer()); + LOG_WARNING("TransportManager connected: wmoR=", (renderer_->getWMORenderer() ? "yes" : "NULL"), + " m2R=", (renderer_->getM2Renderer() ? "yes" : "NULL")); + } + + // Set up NPC animation callbacks (for online creatures) + showProgress("Preparing creatures...", 0.97f); + if (gameHandler_ && renderer_ && renderer_->getCharacterRenderer()) { + auto* cr = renderer_->getCharacterRenderer(); + auto* spawner = entitySpawner_; + + gameHandler_->setNpcDeathCallback([cr, spawner](uint64_t guid) { + spawner->markCreatureDead(guid); + uint32_t instanceId = spawner->getCreatureInstanceId(guid); + if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid); + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death + } + }); + + gameHandler_->setNpcRespawnCallback([cr, spawner](uint64_t guid) { + spawner->unmarkCreatureDead(guid); + uint32_t instanceId = spawner->getCreatureInstanceId(guid); + if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid); + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle + } + }); + + gameHandler_->setNpcSwingCallback([cr, spawner](uint64_t guid) { + uint32_t instanceId = spawner->getCreatureInstanceId(guid); + if (instanceId == 0) instanceId = spawner->getPlayerInstanceId(guid); + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1 + } + }); + } + + // Keep the loading screen visible until all spawn/equipment/gameobject queues + // are fully drained. This ensures the player sees a fully populated world + // (character clothed, NPCs placed, game objects loaded) when the screen drops. + { + const float kMinWarmupSeconds = 2.0f; // minimum time to drain network packets + const float kMaxWarmupSeconds = 25.0f; // hard cap to avoid infinite stall + const auto warmupStart = std::chrono::high_resolution_clock::now(); + // Track consecutive idle iterations (all queues empty) to detect convergence + int idleIterations = 0; + const int kIdleThreshold = 5; // require 5 consecutive empty loops (~80ms) + + while (true) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + window_->setShouldClose(true); + if (loadingScreenOk) loadingScreen.shutdown(); + return; + } + if (event.type == SDL_WINDOWEVENT && + event.window.event == SDL_WINDOWEVENT_RESIZED) { + int w = event.window.data1; + int h = event.window.data2; + window_->setSize(w, h); + if (renderer_ && renderer_->getCamera()) { + renderer_->getCamera()->setAspectRatio(static_cast(w) / h); + } + } + } + + // Drain network and process deferred spawn/composite queues while hidden. + if (gameHandler_) gameHandler_->update(1.0f / 60.0f); + + // If a new world entry was deferred during packet processing, + // stop warming up this map — we'll load the new one after cleanup. + if (pendingWorldEntry_) { + LOG_WARNING("loadOnlineWorldTerrain(map ", mapId, + ") — deferred world entry pending, stopping warmup"); + break; + } + + if (world_) world_->update(1.0f / 60.0f); + + // Process all spawn/equipment/transport queues during warmup + entitySpawner_->update(); + if (auto* cr = renderer_ ? renderer_->getCharacterRenderer() : nullptr) { + cr->processPendingNormalMaps(4); + } + app_.updateQuestMarkers(); + + // Update renderer (terrain streaming, animations) + if (renderer_) { + renderer_->update(1.0f / 60.0f); + } + + const auto now = std::chrono::high_resolution_clock::now(); + const float elapsed = std::chrono::duration(now - warmupStart).count(); + + // Check if all queues are drained + bool queuesEmpty = !entitySpawner_->hasWorkPending(); + + if (queuesEmpty) { + idleIterations++; + } else { + idleIterations = 0; + } + + // Don't exit warmup until the ground under the player exists. + // In cities like Stormwind, players stand on WMO floors, not terrain. + // Check BOTH terrain AND WMO floor — require at least one to be valid. + bool groundReady = false; + if (renderer_) { + glm::vec3 renderSpawn = core::coords::canonicalToRender( + glm::vec3(x, y, z)); + float rx = renderSpawn.x, ry = renderSpawn.y, rz = renderSpawn.z; + + // Check WMO floor FIRST (cities like Stormwind stand on WMO floors). + // Terrain exists below WMOs but at the wrong height. + if (auto* wmo = renderer_->getWMORenderer()) { + auto wmoH = wmo->getFloorHeight(rx, ry, rz + 5.0f); + if (wmoH.has_value() && std::abs(*wmoH - rz) < 15.0f) { + groundReady = true; + } + } + // Check terrain — but only if it's close to spawn Z (within 15 units). + // Terrain far below a WMO city doesn't count as ground. + if (!groundReady) { + if (auto* tm = renderer_->getTerrainManager()) { + auto tH = tm->getHeightAt(rx, ry); + if (tH.has_value() && std::abs(*tH - rz) < 15.0f) { + groundReady = true; + } + } + } + // After 5s with enough tiles loaded, accept terrain as ready even if + // the height sample doesn't match spawn Z exactly. This handles cases + // where getHeightAt returns a slightly different value than the server's + // spawn Z (e.g. terrain LOD, MCNK chunk boundaries, or spawn inside a + // building where floor height differs from terrain below). + if (!groundReady && elapsed >= 5.0f) { + if (auto* tm = renderer_->getTerrainManager()) { + if (tm->getLoadedTileCount() >= 4) { + groundReady = true; + LOG_WARNING("Warmup: using tile-count fallback (", tm->getLoadedTileCount(), " tiles) after ", elapsed, "s"); + } + } + } + + if (!groundReady && elapsed > 5.0f && static_cast(elapsed * 2) % 3 == 0) { + LOG_WARNING("Warmup: ground not ready at spawn (", rx, ",", ry, ",", rz, + ") after ", elapsed, "s"); + } + } + + // Exit when: (min time passed AND queues drained AND ground ready) OR hard cap + bool readyToExit = (elapsed >= kMinWarmupSeconds && idleIterations >= kIdleThreshold && groundReady); + if (readyToExit || elapsed >= kMaxWarmupSeconds) { + if (elapsed >= kMaxWarmupSeconds && !groundReady) { + LOG_WARNING("Warmup hit hard cap (", kMaxWarmupSeconds, "s), ground NOT ready — may fall through world"); + } else if (elapsed >= kMaxWarmupSeconds) { + LOG_WARNING("Warmup hit hard cap (", kMaxWarmupSeconds, "s), entering world with pending work"); + } + break; + } + + const float t = std::clamp(elapsed / kMaxWarmupSeconds, 0.0f, 1.0f); + showProgress("Finalizing world sync...", 0.97f + t * 0.025f); + SDL_Delay(16); + } + } + + // Start intro pan right before entering gameplay so it's visible after loading. + if (renderer_->getCameraController()) { + renderer_->getCameraController()->startIntroPan(2.8f, 140.0f); + } + + showProgress("Entering world...", 1.0f); + + // Ensure all GPU resources (textures, buffers, pipelines) created during + // world load are fully flushed before the first render frame. Without this, + // vkCmdBeginRenderPass can crash on NVIDIA 590.x when resources from async + // uploads haven't completed their queue operations. + if (renderer_ && renderer_->getVkContext()) { + vkDeviceWaitIdle(renderer_->getVkContext()->getDevice()); + } + + if (loadingScreenOk) { + loadingScreen.shutdown(); + } + + // Track which map we actually loaded (used by same-map teleport check). + loadedMapId_ = mapId; + + // Clear loading flag and process any deferred world entry. + // A deferred entry occurs when SMSG_NEW_WORLD arrived during our warmup + // (e.g., an area trigger in a dungeon immediately teleporting the player out). + loadingWorld_ = false; + if (pendingWorldEntry_) { + auto entry = *pendingWorldEntry_; + pendingWorldEntry_.reset(); + LOG_WARNING("Processing deferred world entry: map ", entry.mapId); + app_.worldEntryMovementGraceTimer_ = 2.0f; + app_.taxiLandingClampTimer_ = 0.0f; + app_.lastTaxiFlight_ = false; + // Recursive call — sets loadedMapId_ and IN_GAME state for the final map. + loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z); + return; // The recursive call handles setState(IN_GAME). + } + + // Only enter IN_GAME when this is the final map (no deferred entry pending). + app_.setState(AppState::IN_GAME); + + // Load addons once per session on first world entry + if (addonManager_ && !app_.addonsLoaded_) { + // Set character name for per-character SavedVariables + if (gameHandler_) { + const std::string& charName = gameHandler_->lookupName(gameHandler_->getPlayerGuid()); + if (!charName.empty()) { + addonManager_->setCharacterName(charName); + } else { + // Fallback: find name from character list + for (const auto& c : gameHandler_->getCharacters()) { + if (c.guid == gameHandler_->getPlayerGuid()) { + addonManager_->setCharacterName(c.name); + break; + } + } + } + } + addonManager_->loadAllAddons(); + app_.addonsLoaded_ = true; + addonManager_->fireEvent("VARIABLES_LOADED"); + addonManager_->fireEvent("PLAYER_LOGIN"); + addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); + } else if (addonManager_ && app_.addonsLoaded_) { + // Subsequent world entries (e.g. teleport, instance entry) + addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); + } +} + +void WorldLoader::startWorldPreload(uint32_t mapId, const std::string& mapName, + float serverX, float serverY) { + cancelWorldPreload(); + if (!assetManager_ || !assetManager_->isInitialized() || mapName.empty()) return; + + glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, 0.0f)); + auto [tileX, tileY] = core::coords::canonicalToTile(canonical.x, canonical.y); + + worldPreload_ = std::make_unique(); + worldPreload_->mapId = mapId; + worldPreload_->mapName = mapName; + worldPreload_->centerTileX = tileX; + worldPreload_->centerTileY = tileY; + + LOG_INFO("World preload: starting for map '", mapName, "' tile [", tileX, ",", tileY, "]"); + + // Build list of tiles to preload (radius 1 = 3x3 = 9 tiles, matching load screen) + struct TileJob { int x, y; }; + auto jobs = std::make_shared>(); + // Center tile first (most important) + jobs->push_back({tileX, tileY}); + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + if (dx == 0 && dy == 0) continue; + int tx = tileX + dx, ty = tileY + dy; + if (tx < 0 || tx > 63 || ty < 0 || ty > 63) continue; + jobs->push_back({tx, ty}); + } + } + + // Spawn worker threads (one per tile for maximum parallelism) + auto cancelFlag = &worldPreload_->cancel; + auto* am = assetManager_; + std::string mn = mapName; + + int numWorkers = std::min(static_cast(jobs->size()), 4); + auto nextJob = std::make_shared>(0); + + for (int w = 0; w < numWorkers; w++) { + worldPreload_->workers.emplace_back([am, mn, jobs, nextJob, cancelFlag]() { + while (!cancelFlag->load(std::memory_order_relaxed)) { + int idx = nextJob->fetch_add(1, std::memory_order_relaxed); + if (idx >= static_cast(jobs->size())) break; + + int tx = (*jobs)[idx].x; + int ty = (*jobs)[idx].y; + + // Read ADT file (warms file cache) + std::string adtPath = "World\\Maps\\" + mn + "\\" + mn + "_" + + std::to_string(tx) + "_" + std::to_string(ty) + ".adt"; + am->readFile(adtPath); + if (cancelFlag->load(std::memory_order_relaxed)) break; + + // Read obj0 variant + std::string objPath = "World\\Maps\\" + mn + "\\" + mn + "_" + + std::to_string(tx) + "_" + std::to_string(ty) + "_obj0.adt"; + am->readFile(objPath); + } + LOG_DEBUG("World preload worker finished"); + }); + } +} + +void WorldLoader::cancelWorldPreload() { + if (!worldPreload_) return; + worldPreload_->cancel.store(true, std::memory_order_relaxed); + for (auto& t : worldPreload_->workers) { + if (t.joinable()) t.join(); + } + LOG_INFO("World preload: cancelled (map=", worldPreload_->mapName, + " tile=[", worldPreload_->centerTileX, ",", worldPreload_->centerTileY, "])"); + worldPreload_.reset(); +} + +void WorldLoader::saveLastWorldInfo(uint32_t mapId, const std::string& mapName, + float serverX, float serverY) { +#ifdef _WIN32 + const char* base = std::getenv("APPDATA"); + std::string dir = base ? std::string(base) + "\\wowee" : "."; +#else + const char* home = std::getenv("HOME"); + std::string dir = home ? std::string(home) + "/.wowee" : "."; +#endif + std::filesystem::create_directories(dir); + std::ofstream f(dir + "/last_world.cfg"); + if (f) { + f << mapId << "\n" << mapName << "\n" << serverX << "\n" << serverY << "\n"; + } +} + +WorldLoader::LastWorldInfo WorldLoader::loadLastWorldInfo() const { +#ifdef _WIN32 + const char* base = std::getenv("APPDATA"); + std::string dir = base ? std::string(base) + "\\wowee" : "."; +#else + const char* home = std::getenv("HOME"); + std::string dir = home ? std::string(home) + "/.wowee" : "."; +#endif + LastWorldInfo info; + std::ifstream f(dir + "/last_world.cfg"); + if (!f) return info; + std::string line; + try { + if (std::getline(f, line)) info.mapId = static_cast(std::stoul(line)); + if (std::getline(f, line)) info.mapName = line; + if (std::getline(f, line)) info.x = std::stof(line); + if (std::getline(f, line)) info.y = std::stof(line); + } catch (...) { + LOG_WARNING("Malformed last_world.cfg, ignoring saved position"); + return info; + } + info.valid = !info.mapName.empty(); + return info; +} + +} // namespace core +} // namespace wowee From d43397163e3c0a0b4e5f48e8ceda14ad366f45b7 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 1 Apr 2026 20:38:37 +0300 Subject: [PATCH 3/4] `refactor: decouple Application singleton by extracting core subsystems and updating interfaces` - Add `audio::AudioCoordinator` interface and implementation - Modify `Application` to reduce singleton usage and move controller responsibilities: - application.hpp - application.cpp - Update UI and audio headers/sources: - game_screen.hpp - game_screen.cpp - ui_manager.hpp - audio_coordinator.hpp - audio_coordinator.cpp - Project config touched: - CMakeLists.txt --- CMakeLists.txt | 1 + include/audio/audio_coordinator.hpp | 66 ++++++++++++++++++++++ include/core/application.hpp | 6 +- include/ui/game_screen.hpp | 6 ++ include/ui/ui_manager.hpp | 7 ++- src/audio/audio_coordinator.cpp | 87 +++++++++++++++++++++++++++++ src/core/application.cpp | 6 ++ src/ui/game_screen.cpp | 3 +- 8 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 include/audio/audio_coordinator.hpp create mode 100644 src/audio/audio_coordinator.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 83fccadd..dbb31af1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -544,6 +544,7 @@ set(WOWEE_SOURCES # Audio src/audio/audio_engine.cpp + src/audio/audio_coordinator.cpp src/audio/music_manager.cpp src/audio/footstep_manager.cpp src/audio/activity_sound_manager.cpp diff --git a/include/audio/audio_coordinator.hpp b/include/audio/audio_coordinator.hpp new file mode 100644 index 00000000..f181164f --- /dev/null +++ b/include/audio/audio_coordinator.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +namespace wowee { +namespace pipeline { class AssetManager; } +namespace audio { + +class MusicManager; +class FootstepManager; +class ActivitySoundManager; +class MountSoundManager; +class NpcVoiceManager; +class AmbientSoundManager; +class UiSoundManager; +class CombatSoundManager; +class SpellSoundManager; +class MovementSoundManager; + +/// Coordinates all audio subsystems. +/// Extracted from Renderer to separate audio lifecycle from rendering. +/// Owned by Application; Renderer and UI components access through Application. +class AudioCoordinator { +public: + AudioCoordinator(); + ~AudioCoordinator(); + + /// Initialize the audio engine and all managers. + /// @return true if audio is available (engine initialized successfully) + bool initialize(); + + /// Initialize managers that need AssetManager (music lookups, sound banks). + void initializeWithAssets(pipeline::AssetManager* assetManager); + + /// Shutdown all audio managers and engine. + void shutdown(); + + // Accessors for all audio managers (same interface as Renderer had) + MusicManager* getMusicManager() { return musicManager_.get(); } + FootstepManager* getFootstepManager() { return footstepManager_.get(); } + ActivitySoundManager* getActivitySoundManager() { return activitySoundManager_.get(); } + MountSoundManager* getMountSoundManager() { return mountSoundManager_.get(); } + NpcVoiceManager* getNpcVoiceManager() { return npcVoiceManager_.get(); } + AmbientSoundManager* getAmbientSoundManager() { return ambientSoundManager_.get(); } + UiSoundManager* getUiSoundManager() { return uiSoundManager_.get(); } + CombatSoundManager* getCombatSoundManager() { return combatSoundManager_.get(); } + SpellSoundManager* getSpellSoundManager() { return spellSoundManager_.get(); } + MovementSoundManager* getMovementSoundManager() { return movementSoundManager_.get(); } + +private: + std::unique_ptr musicManager_; + std::unique_ptr footstepManager_; + std::unique_ptr activitySoundManager_; + std::unique_ptr mountSoundManager_; + std::unique_ptr npcVoiceManager_; + std::unique_ptr ambientSoundManager_; + std::unique_ptr uiSoundManager_; + std::unique_ptr combatSoundManager_; + std::unique_ptr spellSoundManager_; + std::unique_ptr movementSoundManager_; + + bool audioAvailable_ = false; +}; + +} // namespace audio +} // namespace wowee diff --git a/include/core/application.hpp b/include/core/application.hpp index 2f47e489..148131ac 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -29,7 +29,7 @@ namespace ui { class UIManager; } namespace auth { class AuthHandler; } namespace game { class GameHandler; class World; class ExpansionRegistry; } namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; struct WMOModel; } -namespace audio { enum class VoiceType; } +namespace audio { enum class VoiceType; class AudioCoordinator; } namespace addons { class AddonManager; } namespace core { @@ -104,6 +104,9 @@ public: // World loader access WorldLoader* getWorldLoader() { return worldLoader_.get(); } + // Audio coordinator access (Section 4.1: extracted audio subsystem) + audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_.get(); } + private: void update(float deltaTime); void render(); @@ -129,6 +132,7 @@ private: std::unique_ptr entitySpawner_; std::unique_ptr appearanceComposer_; std::unique_ptr worldLoader_; + std::unique_ptr audioCoordinator_; AppState state = AppState::AUTHENTICATION; bool running = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 0c29e66f..530d9778 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -23,6 +23,7 @@ #include namespace wowee { +namespace core { class AppearanceComposer; } namespace pipeline { class AssetManager; } namespace rendering { class Renderer; } namespace ui { @@ -50,7 +51,12 @@ public: void saveSettings(); void loadSettings(); + // Dependency injection for extracted classes (Phase A singleton breaking) + void setAppearanceComposer(core::AppearanceComposer* ac) { appearanceComposer_ = ac; } + private: + // Injected dependencies (replaces getInstance() calls) + core::AppearanceComposer* appearanceComposer_ = nullptr; // Chat panel (extracted from GameScreen — owns all chat state and rendering) ChatPanel chatPanel_; diff --git a/include/ui/ui_manager.hpp b/include/ui/ui_manager.hpp index ba1e4c09..f34228d7 100644 --- a/include/ui/ui_manager.hpp +++ b/include/ui/ui_manager.hpp @@ -13,7 +13,7 @@ union SDL_Event; namespace wowee { // Forward declarations -namespace core { class Window; enum class AppState; } +namespace core { class Window; class AppearanceComposer; enum class AppState; } namespace auth { class AuthHandler; } namespace game { class GameHandler; } @@ -69,6 +69,11 @@ public: CharacterScreen& getCharacterScreen() { return *characterScreen; } GameScreen& getGameScreen() { return *gameScreen; } + // Dependency injection forwarding (Phase A singleton breaking) + void setAppearanceComposer(core::AppearanceComposer* ac) { + if (gameScreen) gameScreen->setAppearanceComposer(ac); + } + private: core::Window* window = nullptr; diff --git a/src/audio/audio_coordinator.cpp b/src/audio/audio_coordinator.cpp new file mode 100644 index 00000000..346dd3fe --- /dev/null +++ b/src/audio/audio_coordinator.cpp @@ -0,0 +1,87 @@ +#include "audio/audio_coordinator.hpp" +#include "audio/audio_engine.hpp" +#include "audio/music_manager.hpp" +#include "audio/footstep_manager.hpp" +#include "audio/activity_sound_manager.hpp" +#include "audio/mount_sound_manager.hpp" +#include "audio/npc_voice_manager.hpp" +#include "audio/ambient_sound_manager.hpp" +#include "audio/ui_sound_manager.hpp" +#include "audio/combat_sound_manager.hpp" +#include "audio/spell_sound_manager.hpp" +#include "audio/movement_sound_manager.hpp" +#include "pipeline/asset_manager.hpp" +#include "core/logger.hpp" + +namespace wowee { +namespace audio { + +AudioCoordinator::AudioCoordinator() = default; + +AudioCoordinator::~AudioCoordinator() { + shutdown(); +} + +bool AudioCoordinator::initialize() { + // Initialize AudioEngine (singleton) + if (!AudioEngine::instance().initialize()) { + LOG_WARNING("Failed to initialize AudioEngine - audio will be disabled"); + audioAvailable_ = false; + return false; + } + audioAvailable_ = true; + + // Create all audio managers (initialized later with asset manager) + musicManager_ = std::make_unique(); + footstepManager_ = std::make_unique(); + activitySoundManager_ = std::make_unique(); + mountSoundManager_ = std::make_unique(); + npcVoiceManager_ = std::make_unique(); + ambientSoundManager_ = std::make_unique(); + uiSoundManager_ = std::make_unique(); + combatSoundManager_ = std::make_unique(); + spellSoundManager_ = std::make_unique(); + movementSoundManager_ = std::make_unique(); + + LOG_INFO("AudioCoordinator initialized with ", 10, " audio managers"); + return true; +} + +void AudioCoordinator::initializeWithAssets(pipeline::AssetManager* assetManager) { + if (!audioAvailable_ || !assetManager) return; + + // MusicManager needs asset manager for zone music lookups + if (musicManager_) { + musicManager_->initialize(assetManager); + } + + // Other managers may need asset manager for sound bank loading + // (Add similar calls as needed for other managers) + + LOG_INFO("AudioCoordinator initialized with asset manager"); +} + +void AudioCoordinator::shutdown() { + // Reset all managers first (they may reference AudioEngine) + movementSoundManager_.reset(); + spellSoundManager_.reset(); + combatSoundManager_.reset(); + uiSoundManager_.reset(); + ambientSoundManager_.reset(); + npcVoiceManager_.reset(); + mountSoundManager_.reset(); + activitySoundManager_.reset(); + footstepManager_.reset(); + musicManager_.reset(); + + // Shutdown audio engine last + if (audioAvailable_) { + AudioEngine::instance().shutdown(); + audioAvailable_ = false; + } + + LOG_INFO("AudioCoordinator shutdown complete"); +} + +} // namespace audio +} // namespace wowee diff --git a/src/core/application.cpp b/src/core/application.cpp index a91f8223..91a0aaff 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -31,6 +31,7 @@ #include "audio/footstep_manager.hpp" #include "audio/activity_sound_manager.hpp" #include "audio/audio_engine.hpp" +#include "audio/audio_coordinator.hpp" #include "addons/addon_manager.hpp" #include #include "pipeline/m2_loader.hpp" @@ -223,6 +224,11 @@ bool Application::initialize() { renderer.get(), assetManager.get(), gameHandler.get(), dbcLayout_.get(), entitySpawner_.get()); + // Wire AppearanceComposer to UI components (Phase A singleton breaking) + if (uiManager) { + uiManager->setAppearanceComposer(appearanceComposer_.get()); + } + // Ensure the main in-world CharacterRenderer can load textures immediately. // Previously this was only wired during terrain initialization, which meant early spawns // (before terrain load) would render with white fallback textures (notably hair). diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e7a2e271..d18566fb 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2,6 +2,7 @@ #include "ui/ui_colors.hpp" #include "rendering/vk_context.hpp" #include "core/application.hpp" +#include "core/appearance_composer.hpp" #include "addons/addon_manager.hpp" #include "core/coordinates.hpp" #include "core/input.hpp" @@ -631,7 +632,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) { updateCharacterGeosets(gameHandler.getInventory()); updateCharacterTextures(gameHandler.getInventory()); - if (auto* ac = core::Application::getInstance().getAppearanceComposer()) ac->loadEquippedWeapons(); + if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); inventoryScreen.markPreviewDirty(); // Update renderer weapon type for animation selection auto* r = core::Application::getInstance().getRenderer(); From 1c0e9dd1df935dd20e340050155d35bb70fe62b5 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 1 Apr 2026 20:59:17 +0300 Subject: [PATCH 4/4] `chore(application): extract appearance controller and unify UI flow` - Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates - Update UI components and managers to use new service layer: - `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager` - Adjust core application entrypoints: - application.cpp - Update component implementations for new controller flow: - action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp` These staged changes implement a major architectural refactor for UI/appearance controller separation --- include/ui/action_bar_panel.hpp | 5 ++ include/ui/auth_screen.hpp | 6 ++ include/ui/character_screen.hpp | 6 ++ include/ui/chat_panel.hpp | 7 +++ include/ui/combat_ui.hpp | 7 +++ include/ui/dialog_manager.hpp | 6 ++ include/ui/game_screen.hpp | 8 ++- include/ui/settings_panel.hpp | 6 ++ include/ui/social_panel.hpp | 7 +++ include/ui/toast_manager.hpp | 7 +++ include/ui/ui_manager.hpp | 11 ++++ include/ui/ui_services.hpp | 55 +++++++++++++++++ include/ui/window_manager.hpp | 5 ++ src/core/application.cpp | 31 ++++++++++ src/ui/action_bar_panel.cpp | 10 +-- src/ui/chat_panel.cpp | 24 ++++---- src/ui/combat_ui.cpp | 20 +++--- src/ui/dialog_manager.cpp | 34 +++++----- src/ui/game_screen.cpp | 106 ++++++++++++++++++-------------- src/ui/settings_panel.cpp | 10 +-- src/ui/social_panel.cpp | 18 +++--- src/ui/toast_manager.cpp | 22 +++---- src/ui/window_manager.cpp | 38 ++++++------ 23 files changed, 315 insertions(+), 134 deletions(-) create mode 100644 include/ui/ui_services.hpp diff --git a/include/ui/action_bar_panel.hpp b/include/ui/action_bar_panel.hpp index ae650485..4a62e642 100644 --- a/include/ui/action_bar_panel.hpp +++ b/include/ui/action_bar_panel.hpp @@ -4,6 +4,7 @@ // XP bar, reputation bar, macro resolution. // ============================================================ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -70,7 +71,11 @@ public: std::unordered_map macroPrimarySpellCache_; size_t macroCacheSpellCount_ = 0; + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + private: + UIServices services_; uint32_t resolveMacroPrimarySpellId(uint32_t macroId, game::GameHandler& gameHandler); }; diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index e1dbdde7..ff99d963 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include "auth/auth_handler.hpp" #include #include @@ -30,6 +31,9 @@ public: */ void setOnSuccess(std::function callback) { onSuccess = callback; } + /// Set services (dependency injection) + void setServices(const UIServices& services) { services_ = services; } + /** * Check if authentication is in progress @@ -44,6 +48,8 @@ public: const std::string& getStatusMessage() const { return statusMessage; } private: + UIServices services_; // Injected service references + struct ServerProfile { std::string hostname; int port = 3724; diff --git a/include/ui/character_screen.hpp b/include/ui/character_screen.hpp index aae4d6a0..c4312bc8 100644 --- a/include/ui/character_screen.hpp +++ b/include/ui/character_screen.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include "game/game_handler.hpp" #include #include @@ -48,6 +49,9 @@ public: void setOnBack(std::function cb) { onBack = std::move(cb); } void setOnDeleteCharacter(std::function cb) { onDeleteCharacter = std::move(cb); } + /// Set services (dependency injection) + void setServices(const UIServices& services) { services_ = services; } + /** * Reset selection state (e.g., when switching servers) */ @@ -89,6 +93,8 @@ public: void selectCharacterByName(const std::string& name); private: + UIServices services_; // Injected service references + // UI state int selectedCharacterIndex = -1; bool characterSelected = false; diff --git a/include/ui/chat_panel.hpp b/include/ui/chat_panel.hpp index 50dac84b..b6528545 100644 --- a/include/ui/chat_panel.hpp +++ b/include/ui/chat_panel.hpp @@ -1,6 +1,7 @@ #pragma once #include "game/game_handler.hpp" +#include "ui/ui_services.hpp" #include #include #include @@ -109,10 +110,16 @@ public: /** Reset all chat settings to defaults. */ void restoreDefaults(); + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + /** Replace $g/$G and $n/$N gender/name placeholders in quest/chat text. */ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler); private: + // Section 3.5: Injected UI services (Phase B singleton breaking) + UIServices services_; + // ---- Chat input state ---- char chatInputBuffer_[512] = ""; char whisperTargetBuffer_[256] = ""; diff --git a/include/ui/combat_ui.hpp b/include/ui/combat_ui.hpp index 7d7a8058..b09d2fc4 100644 --- a/include/ui/combat_ui.hpp +++ b/include/ui/combat_ui.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -70,6 +71,12 @@ public: SpellbookScreen& spellbookScreen); void renderThreatWindow(game::GameHandler& gameHandler); void renderBgScoreboard(game::GameHandler& gameHandler); + + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + +private: + UIServices services_; }; } // namespace ui diff --git a/include/ui/dialog_manager.hpp b/include/ui/dialog_manager.hpp index 2fd0fb19..9bd075ee 100644 --- a/include/ui/dialog_manager.hpp +++ b/include/ui/dialog_manager.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -34,7 +35,12 @@ public: /// called in render() after reclaim corpse button void renderLateDialogs(game::GameHandler& gameHandler); + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + private: + // Section 3.5: Injected UI services + UIServices services_; // Common ImGui window flags for popup dialogs static constexpr ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 530d9778..f35bd679 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -17,6 +17,7 @@ #include "ui/social_panel.hpp" #include "ui/action_bar_panel.hpp" #include "ui/window_manager.hpp" +#include "ui/ui_services.hpp" #include #include #include @@ -54,8 +55,13 @@ public: // Dependency injection for extracted classes (Phase A singleton breaking) void setAppearanceComposer(core::AppearanceComposer* ac) { appearanceComposer_ = ac; } + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services); + private: - // Injected dependencies (replaces getInstance() calls) + // Injected UI services (Section 3.5 Phase B - replaces getInstance() calls) + UIServices services_; + // Legacy pointer for Phase A compatibility (will be removed when all callsites migrate) core::AppearanceComposer* appearanceComposer_ = nullptr; // Chat panel (extracted from GameScreen — owns all chat state and rendering) ChatPanel chatPanel_; diff --git a/include/ui/settings_panel.hpp b/include/ui/settings_panel.hpp index 36d92f85..5dd58136 100644 --- a/include/ui/settings_panel.hpp +++ b/include/ui/settings_panel.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -149,7 +150,12 @@ public: /// Return the platform-specific settings file path static std::string getSettingsPath(); + /// Set services (dependency injection) + void setServices(const UIServices& services) { services_ = services; } + private: + UIServices services_; // Injected service references + // Keybinding customization (private — only used in Controls tab) int pendingRebindAction_ = -1; // -1 = not rebinding, otherwise action index bool awaitingKeyPress_ = false; diff --git a/include/ui/social_panel.hpp b/include/ui/social_panel.hpp index 30bce495..1eed9a93 100644 --- a/include/ui/social_panel.hpp +++ b/include/ui/social_panel.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -71,6 +72,12 @@ public: ChatPanel& chatPanel); void renderInspectWindow(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen); + + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + +private: + UIServices services_; }; } // namespace ui diff --git a/include/ui/toast_manager.hpp b/include/ui/toast_manager.hpp index 29c27983..34cafa4d 100644 --- a/include/ui/toast_manager.hpp +++ b/include/ui/toast_manager.hpp @@ -1,5 +1,6 @@ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -40,11 +41,17 @@ public: /// Fire achievement earned toast + sound void triggerAchievementToast(uint32_t achievementId, std::string name = {}); + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + // --- public state consumed by GameScreen for the golden burst overlay --- float levelUpFlashAlpha = 0.0f; uint32_t levelUpDisplayLevel = 0; private: + // Section 3.5: Injected UI services + UIServices services_; + // ---- Ding effect (own level-up) ---- static constexpr float DING_DURATION = 4.0f; float dingTimer_ = 0.0f; diff --git a/include/ui/ui_manager.hpp b/include/ui/ui_manager.hpp index f34228d7..bdc217d9 100644 --- a/include/ui/ui_manager.hpp +++ b/include/ui/ui_manager.hpp @@ -5,6 +5,7 @@ #include "ui/character_create_screen.hpp" #include "ui/character_screen.hpp" #include "ui/game_screen.hpp" +#include "ui/ui_services.hpp" #include // Forward declare SDL_Event @@ -74,8 +75,18 @@ public: if (gameScreen) gameScreen->setAppearanceComposer(ac); } + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { + services_ = services; + if (gameScreen) gameScreen->setServices(services); + if (authScreen) authScreen->setServices(services); + if (characterScreen) characterScreen->setServices(services); + } + const UIServices& getServices() const { return services_; } + private: core::Window* window = nullptr; + UIServices services_; // Section 3.5: Injected services // UI Screens std::unique_ptr authScreen; diff --git a/include/ui/ui_services.hpp b/include/ui/ui_services.hpp new file mode 100644 index 00000000..fdcd4b2b --- /dev/null +++ b/include/ui/ui_services.hpp @@ -0,0 +1,55 @@ +#pragma once + +namespace wowee { + +// Forward declarations +namespace core { + class Window; + class EntitySpawner; + class AppearanceComposer; + class WorldLoader; +} +namespace rendering { class Renderer; } +namespace pipeline { class AssetManager; } +namespace game { + class GameHandler; + class ExpansionRegistry; +} +namespace addons { class AddonManager; } +namespace audio { class AudioCoordinator; } + +namespace ui { + +/** + * UI Services - Dependency injection container for UI components. + * + * Section 3.5: Break the singleton Phase B + * + * Replaces Application::getInstance() calls throughout UI code. + * Application creates this struct and injects it into UIManager, + * which propagates it to GameScreen and all child UI components. + * + * Owned by Application, shared as const pointers (non-owning). + */ +struct UIServices { + core::Window* window = nullptr; + rendering::Renderer* renderer = nullptr; + pipeline::AssetManager* assetManager = nullptr; + game::GameHandler* gameHandler = nullptr; + game::ExpansionRegistry* expansionRegistry = nullptr; + addons::AddonManager* addonManager = nullptr; + audio::AudioCoordinator* audioCoordinator = nullptr; + + // Extracted classes (also available individually for Phase A compatibility) + core::EntitySpawner* entitySpawner = nullptr; + core::AppearanceComposer* appearanceComposer = nullptr; + core::WorldLoader* worldLoader = nullptr; + + // Helper to check if core services are wired + bool isValid() const { + return window && renderer && assetManager && gameHandler; + } +}; + +} // namespace ui +} // namespace wowee diff --git a/include/ui/window_manager.hpp b/include/ui/window_manager.hpp index f899910b..a5f3564c 100644 --- a/include/ui/window_manager.hpp +++ b/include/ui/window_manager.hpp @@ -7,6 +7,7 @@ // equipment sets, skills. // ============================================================ #pragma once +#include "ui/ui_services.hpp" #include #include #include @@ -173,7 +174,11 @@ public: std::unordered_map extendedCostCache_; bool extendedCostDbLoaded_ = false; + // Section 3.5: UIServices injection (Phase B singleton breaking) + void setServices(const UIServices& services) { services_ = services; } + private: + UIServices services_; void loadExtendedCostDBC(); std::string formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler); }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 91a0aaff..e597cb91 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -39,6 +39,7 @@ #include "pipeline/wdt_loader.hpp" #include "pipeline/dbc_loader.hpp" #include "ui/ui_manager.hpp" +#include "ui/ui_services.hpp" #include "auth/auth_handler.hpp" #include "game/game_handler.hpp" #include "game/transport_manager.hpp" @@ -227,6 +228,20 @@ bool Application::initialize() { // Wire AppearanceComposer to UI components (Phase A singleton breaking) if (uiManager) { uiManager->setAppearanceComposer(appearanceComposer_.get()); + + // Wire all services to UI components (Phase B singleton breaking) + ui::UIServices uiServices; + uiServices.window = window.get(); + uiServices.renderer = renderer.get(); + uiServices.assetManager = assetManager.get(); + uiServices.gameHandler = gameHandler.get(); + uiServices.expansionRegistry = expansionRegistry_.get(); + uiServices.addonManager = addonManager_.get(); // May be nullptr here, re-wire later + uiServices.audioCoordinator = audioCoordinator_.get(); + uiServices.entitySpawner = entitySpawner_.get(); + uiServices.appearanceComposer = appearanceComposer_.get(); + uiServices.worldLoader = worldLoader_.get(); + uiManager->setServices(uiServices); } // Ensure the main in-world CharacterRenderer can load textures immediately. @@ -505,6 +520,22 @@ bool Application::initialize() { entitySpawner_.get(), appearanceComposer_.get(), window.get(), world.get(), addonManager_.get()); + // Re-wire UIServices now that all services (addonManager_, worldLoader_) are available + if (uiManager) { + ui::UIServices uiServices; + uiServices.window = window.get(); + uiServices.renderer = renderer.get(); + uiServices.assetManager = assetManager.get(); + uiServices.gameHandler = gameHandler.get(); + uiServices.expansionRegistry = expansionRegistry_.get(); + uiServices.addonManager = addonManager_.get(); + uiServices.audioCoordinator = audioCoordinator_.get(); + uiServices.entitySpawner = entitySpawner_.get(); + uiServices.appearanceComposer = appearanceComposer_.get(); + uiServices.worldLoader = worldLoader_.get(); + uiManager->setServices(uiServices); + } + // Start background preload for last-played character's world. // Warms the file cache so terrain tile loading is faster at Enter World. { diff --git a/src/ui/action_bar_panel.cpp b/src/ui/action_bar_panel.cpp index 7ab39af4..49460c14 100644 --- a/src/ui/action_bar_panel.cpp +++ b/src/ui/action_bar_panel.cpp @@ -172,7 +172,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler, ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; float slotSize = 48.0f * settingsPanel.pendingActionBarScale; float spacing = 4.0f; @@ -1107,7 +1107,7 @@ void ActionBarPanel::renderStanceBar(game::GameHandler& gameHandler, ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; // Match the action bar slot size so they align neatly float slotSize = 38.0f; @@ -1196,7 +1196,7 @@ void ActionBarPanel::renderBagBar(game::GameHandler& gameHandler, ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; float slotSize = 42.0f; float spacing = 4.0f; @@ -1232,7 +1232,7 @@ void ActionBarPanel::renderBagBar(game::GameHandler& gameHandler, if (!blpData.empty()) { auto image = pipeline::BLPLoader::load(blpData); if (image.isValid()) { - auto* w = core::Application::getInstance().getWindow(); + auto* w = services_.window; auto* vkCtx = w ? w->getVkContext() : nullptr; if (vkCtx) backpackIconTexture_ = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height); @@ -1483,7 +1483,7 @@ void ActionBarPanel::renderXpBar(game::GameHandler& gameHandler, ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; (void)window; // Not used for positioning; kept for AssetManager if needed // Position just above both action bars (bar1 at screenH-barH, bar2 above that) diff --git a/src/ui/chat_panel.cpp b/src/ui/chat_panel.cpp index 8a6d0ef3..19ff5331 100644 --- a/src/ui/chat_panel.cpp +++ b/src/ui/chat_panel.cpp @@ -197,8 +197,8 @@ void ChatPanel::render(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen, SpellbookScreen& spellbookScreen, QuestLogScreen& questLogScreen) { - auto* window = core::Application::getInstance().getWindow(); - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* window = services_.window; + auto* assetMgr = services_.assetManager; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float chatW = std::min(500.0f, screenW * 0.4f); @@ -1109,7 +1109,7 @@ void ChatPanel::render(game::GameHandler& gameHandler, std::string bodyLower = mMsg.message; for (auto& c : bodyLower) c = static_cast(std::tolower(static_cast(c))); if (bodyLower.find(selfNameLower) != std::string::npos) { - if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* renderer = services_.renderer) { if (auto* ui = renderer->getUiSoundManager()) ui->playWhisperReceived(); } @@ -2151,7 +2151,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, // /run — execute Lua script via addon system if ((cmdLower == "run" || cmdLower == "script") && spacePos != std::string::npos) { std::string luaCode = command.substr(spacePos + 1); - auto* am = core::Application::getInstance().getAddonManager(); + auto* am = services_.addonManager; if (am) { am->runScript(luaCode); } else { @@ -2164,7 +2164,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, // /dump — evaluate Lua expression and print result if ((cmdLower == "dump" || cmdLower == "print") && spacePos != std::string::npos) { std::string expr = command.substr(spacePos + 1); - auto* am = core::Application::getInstance().getAddonManager(); + auto* am = services_.addonManager; if (am && am->isInitialized()) { // Wrap expression in print(tostring(...)) to display the value std::string wrapped = "local __v = " + expr + @@ -2187,7 +2187,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, // Check addon slash commands (SlashCmdList) before built-in commands { - auto* am = core::Application::getInstance().getAddonManager(); + auto* am = services_.addonManager; if (am && am->isInitialized()) { std::string slashCmd = "/" + cmdLower; std::string slashArgs; @@ -2214,7 +2214,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, // /reload or /reloadui — reload all addons (save variables, re-init Lua, re-scan .toc files) if (cmdLower == "reload" || cmdLower == "reloadui" || cmdLower == "rl") { - auto* am = core::Application::getInstance().getAddonManager(); + auto* am = services_.addonManager; if (am) { am->reload(); am->fireEvent("VARIABLES_LOADED"); @@ -2301,7 +2301,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, if (cmdLower == "loc" || cmdLower == "coords" || cmdLower == "whereami") { const auto& pmi = gameHandler.getMovementInfo(); std::string zoneName; - if (auto* rend = core::Application::getInstance().getRenderer()) + if (auto* rend = services_.renderer) zoneName = rend->getCurrentZoneName(); char buf[256]; snprintf(buf, sizeof(buf), "%.1f, %.1f, %.1f%s%s", @@ -2327,7 +2327,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, // /zone command — print current zone name if (cmdLower == "zone") { std::string zoneName; - if (auto* rend = core::Application::getInstance().getRenderer()) + if (auto* rend = services_.renderer) zoneName = rend->getCurrentZoneName(); game::MessageChatData sysMsg; sysMsg.type = game::ChatType::SYSTEM; @@ -4323,7 +4323,7 @@ void ChatPanel::sendChatMessage(game::GameHandler& gameHandler, std::string emoteText = rendering::Renderer::getEmoteText(cmdLower, targetNamePtr); if (!emoteText.empty()) { // Play the emote animation - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { renderer->playEmote(cmdLower); } @@ -4697,11 +4697,11 @@ std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game:: void ChatPanel::renderBubbles(game::GameHandler& gameHandler) { if (chatBubbles_.empty()) return; - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; auto* camera = renderer ? renderer->getCamera() : nullptr; if (!camera) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; diff --git a/src/ui/combat_ui.cpp b/src/ui/combat_ui.cpp index 8ba732e7..17b6e0c9 100644 --- a/src/ui/combat_ui.cpp +++ b/src/ui/combat_ui.cpp @@ -60,7 +60,7 @@ namespace ui { void CombatUI::renderCastBar(game::GameHandler& gameHandler, SpellIconFn getSpellIcon) { if (!gameHandler.isCasting()) return; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; @@ -187,8 +187,8 @@ void CombatUI::renderCooldownTracker(game::GameHandler& gameHandler, return a.remaining > b.remaining; }); - auto* assetMgr = core::Application::getInstance().getAssetManager(); - auto* window = core::Application::getInstance().getWindow(); + auto* assetMgr = services_.assetManager; + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -268,7 +268,7 @@ void CombatUI::renderRaidWarningOverlay(game::GameHandler& gameHandler) { // Walk only the new messages (deque — iterate from back by skipping old ones) size_t toScan = newCount - raidWarnChatSeenCount_; size_t startIdx = newCount > toScan ? newCount - toScan : 0; - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; for (size_t i = startIdx; i < newCount; ++i) { const auto& msg = chatHistory[i]; if (msg.type == game::ChatType::RAID_WARNING || @@ -361,13 +361,13 @@ void CombatUI::renderCombatText(game::GameHandler& gameHandler) { const auto& entries = gameHandler.getCombatText(); if (entries.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (!window) return; const float screenW = static_cast(window->getWidth()); const float screenH = static_cast(window->getHeight()); // Camera for world-space projection - auto* appRenderer = core::Application::getInstance().getRenderer(); + auto* appRenderer = services_.renderer; rendering::Camera* camera = appRenderer ? appRenderer->getCamera() : nullptr; glm::mat4 viewProj; if (camera) viewProj = camera->getProjectionMatrix() * camera->getViewMatrix(); @@ -785,7 +785,7 @@ void CombatUI::renderDPSMeter(game::GameHandler& gameHandler, fmtNum(hps, hpsBuf, sizeof(hpsBuf)); // Position: small floating label just above the action bar, right of center - auto* appWin = core::Application::getInstance().getWindow(); + auto* appWin = services_.window; float screenW = appWin ? static_cast(appWin->getWidth()) : 1280.0f; float screenH = appWin ? static_cast(appWin->getHeight()) : 720.0f; @@ -866,7 +866,7 @@ void CombatUI::renderBuffBar(game::GameHandler& gameHandler, } if (activeCount == 0 && !gameHandler.hasPet()) return; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; // Position below the minimap (minimap: 200x200 at top-right, bottom edge at Y≈210) // Anchored to the right side to stay away from party frames on the left @@ -1201,7 +1201,7 @@ void CombatUI::renderBattlegroundScore(game::GameHandler& gameHandler) { if (auto mv = gameHandler.getWorldState(def->maxKey)) maxScore = *mv; } - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; // Width scales with screen but stays reasonable @@ -1598,7 +1598,7 @@ void CombatUI::renderCombatLog(game::GameHandler& gameHandler, ImGui::TextColored(color, "%s", desc); // Hover tooltip: show rich spell info for entries with a known spell if (e.spellId != 0 && ImGui::IsItemHovered()) { - auto* assetMgrLog = core::Application::getInstance().getAssetManager(); + auto* assetMgrLog = services_.assetManager; ImGui::BeginTooltip(); bool richOk = spellbookScreen.renderSpellInfoTooltip(e.spellId, gameHandler, assetMgrLog); if (!richOk) { diff --git a/src/ui/dialog_manager.cpp b/src/ui/dialog_manager.cpp index dd90a990..0677efb3 100644 --- a/src/ui/dialog_manager.cpp +++ b/src/ui/dialog_manager.cpp @@ -69,7 +69,7 @@ void DialogManager::renderLateDialogs(game::GameHandler& gameHandler) { void DialogManager::renderGroupInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGroupInvite()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always); @@ -93,7 +93,7 @@ void DialogManager::renderGroupInvitePopup(game::GameHandler& gameHandler) { void DialogManager::renderDuelRequestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingDuelRequest()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 250), ImGuiCond_Always); @@ -158,7 +158,7 @@ void DialogManager::renderDuelCountdown(game::GameHandler& gameHandler) { void DialogManager::renderItemTextWindow(game::GameHandler& gameHandler) { if (!gameHandler.isItemTextOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -194,7 +194,7 @@ void DialogManager::renderItemTextWindow(game::GameHandler& gameHandler) { void DialogManager::renderSharedQuestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingSharedQuest()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 490), ImGuiCond_Always); @@ -224,7 +224,7 @@ void DialogManager::renderSummonRequestPopup(game::GameHandler& gameHandler) { gameHandler.tickSummonTimeout(dt); if (!gameHandler.hasPendingSummonRequest()) return; // expired - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 430), ImGuiCond_Always); @@ -252,7 +252,7 @@ void DialogManager::renderSummonRequestPopup(game::GameHandler& gameHandler) { void DialogManager::renderTradeRequestPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingTradeRequest()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 370), ImGuiCond_Always); @@ -284,7 +284,7 @@ void DialogManager::renderTradeWindow(game::GameHandler& gameHandler, const uint64_t peerGold = gameHandler.getPeerTradeGold(); const auto& peerName = gameHandler.getTradePeerName(); - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -443,7 +443,7 @@ void DialogManager::renderLootRollPopup(game::GameHandler& gameHandler, const auto& roll = gameHandler.getPendingLootRoll(); - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 310), ImGuiCond_Always); @@ -584,7 +584,7 @@ void DialogManager::renderLootRollPopup(game::GameHandler& gameHandler, void DialogManager::renderGuildInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGuildInvite()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 250), ImGuiCond_Always); @@ -610,7 +610,7 @@ void DialogManager::renderGuildInvitePopup(game::GameHandler& gameHandler) { void DialogManager::renderReadyCheckPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingReadyCheck()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -684,7 +684,7 @@ void DialogManager::renderBgInvitePopup(game::GameHandler& gameHandler) { return; } - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -745,7 +745,7 @@ void DialogManager::renderBfMgrInvitePopup(game::GameHandler& gameHandler) { // Only shown on WotLK servers (outdoor battlefields like Wintergrasp use the BF Manager) if (!gameHandler.hasBfMgrInvite()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -802,7 +802,7 @@ void DialogManager::renderLfgProposalPopup(game::GameHandler& gameHandler) { using LfgState = game::GameHandler::LfgState; if (gameHandler.getLfgState() != LfgState::Proposal) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -851,7 +851,7 @@ void DialogManager::renderLfgRoleCheckPopup(game::GameHandler& gameHandler) { using LfgState = game::GameHandler::LfgState; if (gameHandler.getLfgState() != LfgState::RoleCheck) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -915,7 +915,7 @@ void DialogManager::renderLfgRoleCheckPopup(game::GameHandler& gameHandler) { void DialogManager::renderResurrectDialog(game::GameHandler& gameHandler) { if (!gameHandler.showResurrectDialog()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -976,7 +976,7 @@ void DialogManager::renderResurrectDialog(game::GameHandler& gameHandler) { void DialogManager::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) { if (!gameHandler.showTalentWipeConfirmDialog()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -1046,7 +1046,7 @@ void DialogManager::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler void DialogManager::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) { if (!gameHandler.showPetUnlearnDialog()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index d18566fb..1119a7d4 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -250,6 +250,22 @@ GameScreen::GameScreen() { loadSettings(); } +// Section 3.5: Set UI services and propagate to child components +void GameScreen::setServices(const UIServices& services) { + services_ = services; + // Update legacy pointer for Phase A compatibility + appearanceComposer_ = services.appearanceComposer; + // Propagate to child panels + chatPanel_.setServices(services); + toastManager_.setServices(services); + dialogManager_.setServices(services); + settingsPanel_.setServices(services); + combatUI_.setServices(services); + socialPanel_.setServices(services); + actionBarPanel_.setServices(services); + windowManager_.setServices(services); +} + void GameScreen::render(game::GameHandler& gameHandler) { // Set up chat bubble callback (once) and cache game handler in ChatPanel chatPanel_.setupCallbacks(gameHandler); @@ -269,7 +285,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { uiErrors_.push_back({msg, 0.0f}); if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin()); // Play error sound for each new error (rate-limited by deque cap of 5) - if (auto* r = core::Application::getInstance().getRenderer()) { + if (auto* r = services_.renderer) { if (auto* sfx = r->getUiSoundManager()) sfx->playError(); } }); @@ -292,7 +308,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Sync minimap opacity with UI opacity { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->setOpacity(settingsPanel_.uiOpacity_); @@ -302,7 +318,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply initial settings when renderer becomes available if (!settingsPanel_.minimapSettingsApplied_) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { if (auto* minimap = renderer->getMinimap()) { settingsPanel_.minimapRotate_ = false; @@ -329,7 +345,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved volume settings once when audio managers first become available if (!settingsPanel_.volumeSettingsApplied_) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer && renderer->getUiSoundManager()) { settingsPanel_.applyAudioVolumes(renderer); settingsPanel_.volumeSettingsApplied_ = true; @@ -338,7 +354,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved MSAA setting once when renderer is available if (!settingsPanel_.msaaSettingsApplied_ && settingsPanel_.pendingAntiAliasing > 0) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { static const VkSampleCountFlagBits aaSamples[] = { VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, @@ -353,7 +369,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved FXAA setting once when renderer is available if (!settingsPanel_.fxaaSettingsApplied_) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { renderer->setFXAAEnabled(settingsPanel_.pendingFXAA); settingsPanel_.fxaaSettingsApplied_ = true; @@ -362,7 +378,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved water refraction setting once when renderer is available if (!settingsPanel_.waterRefractionApplied_) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { renderer->setWaterRefractionEnabled(settingsPanel_.pendingWaterRefraction); settingsPanel_.waterRefractionApplied_ = true; @@ -371,7 +387,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved normal mapping / POM settings once when WMO renderer is available if (!settingsPanel_.normalMapSettingsApplied_) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { if (auto* wr = renderer->getWMORenderer()) { wr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping); @@ -391,7 +407,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved upscaling setting once when renderer is available if (!settingsPanel_.fsrSettingsApplied_) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { static constexpr float fsrScales[] = { 0.77f, 0.67f, 0.59f, 1.00f }; settingsPanel_.pendingFSRQuality = std::clamp(settingsPanel_.pendingFSRQuality, 0, 3); @@ -562,7 +578,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { questLogScreen.render(gameHandler, inventoryScreen); // Spellbook (P key toggle handled inside) - spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager()); + spellbookScreen.render(gameHandler, services_.assetManager); // Insert spell link into chat if player shift-clicked a spellbook entry { @@ -579,7 +595,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { { uint64_t activeGuid = gameHandler.getActiveCharacterGuid(); if (activeGuid != 0 && activeGuid != inventoryScreenCharGuid_) { - auto* am = core::Application::getInstance().getAssetManager(); + auto* am = services_.assetManager; if (am) { inventoryScreen.setAssetManager(am); const auto* ch = gameHandler.getActiveCharacter(); @@ -635,7 +651,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); inventoryScreen.markPreviewDirty(); // Update renderer weapon type for animation selection - auto* r = core::Application::getInstance().getRenderer(); + auto* r = services_.renderer; if (r) { const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND); r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType); @@ -643,7 +659,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { } // Update renderer face-target position and selection circle - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { renderer->setInCombat(gameHandler.isInCombat() && !gameHandler.isPlayerDead() && @@ -1202,9 +1218,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { // Cursor affordance: show hand cursor over interactable entities. if (!io.WantCaptureMouse) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; auto* camera = renderer ? renderer->getCamera() : nullptr; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (camera && window) { glm::vec2 mousePos = input.getMousePosition(); float screenW = static_cast(window->getWidth()); @@ -1258,9 +1274,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { constexpr float CLICK_THRESHOLD = 5.0f; // pixels if (dragDistSq < CLICK_THRESHOLD * CLICK_THRESHOLD) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; auto* camera = renderer ? renderer->getCamera() : nullptr; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (camera && window) { float screenW = static_cast(window->getWidth()); @@ -1355,9 +1371,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { // If no target or right-clicking in world, try to pick one under cursor { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; auto* camera = renderer ? renderer->getCamera() : nullptr; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (camera && window) { // If a quest objective gameobject is under the cursor, prefer it over // hostile units so quest pickups (e.g. "Bundle of Wood") are reliable. @@ -1648,7 +1664,7 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), ""); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Do not disturb — /dnd to cancel"); } - if (auto* ren = core::Application::getInstance().getRenderer()) { + if (auto* ren = services_.renderer) { if (auto* cam = ren->getCameraController()) { if (cam->isAutoRunning()) { ImGui::SameLine(); @@ -2176,7 +2192,7 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { // Raw slot value layout (WotLK 3.3.5): low 24 bits = spell/action ID, // high byte = flag (0x80=autocast on, 0x40=can-autocast, 0x0C=type). // Built-in commands: id=2 follow, id=3 stay/move, id=5 attack. - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; const float iconSz = 20.0f; const float spacing = 2.0f; ImGui::Separator(); @@ -2279,7 +2295,7 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { else if (actionId == 6) tip = "Aggressive"; if (tip) ImGui::SetTooltip("%s", tip); } else if (actionId > 6) { - auto* spellAsset = core::Application::getInstance().getAssetManager(); + auto* spellAsset = services_.assetManager; ImGui::BeginTooltip(); bool richOk = spellbookScreen.renderSpellInfoTooltip(actionId, gameHandler, spellAsset); if (!richOk) { @@ -2400,7 +2416,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { auto target = gameHandler.getTarget(); if (!target) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float frameW = 250.0f; @@ -2839,7 +2855,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { else snprintf(castLabel, sizeof(castLabel), "Casting... (%.1fs)", castLeft); { - auto* tcastAsset = core::Application::getInstance().getAssetManager(); + auto* tcastAsset = services_.assetManager; VkDescriptorSet tIcon = (tspell != 0 && tcastAsset) ? getSpellIcon(tspell, tcastAsset) : VK_NULL_HANDLE; if (tIcon) { @@ -2936,7 +2952,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (!a.isEmpty()) activeAuras++; } if (activeAuras > 0) { - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; constexpr float ICON_SIZE = 24.0f; constexpr int ICONS_PER_ROW = 8; @@ -3234,7 +3250,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { int totActive = 0; for (const auto& a : *totAuras) if (!a.isEmpty()) totActive++; if (totActive > 0) { - auto* totAsset = core::Application::getInstance().getAssetManager(); + auto* totAsset = services_.assetManager; constexpr float TA_ICON = 16.0f; constexpr int TA_PER_ROW = 8; @@ -3350,7 +3366,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { auto focus = gameHandler.getFocus(); if (!focus) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; // Position: right side of screen, mirroring the target frame on the opposite side @@ -3652,7 +3668,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { else snprintf(castBuf, sizeof(castBuf), "Casting... (%.1fs)", rem); { - auto* fcAsset = core::Application::getInstance().getAssetManager(); + auto* fcAsset = services_.assetManager; VkDescriptorSet fcIcon = (focusCast->spellId != 0 && fcAsset) ? getSpellIcon(focusCast->spellId, fcAsset) : VK_NULL_HANDLE; if (fcIcon) { @@ -3678,7 +3694,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { int activeCount = 0; for (const auto& a : *focusAuras) if (!a.isEmpty()) activeCount++; if (activeCount > 0) { - auto* focusAsset = core::Application::getInstance().getAssetManager(); + auto* focusAsset = services_.assetManager; constexpr float FA_ICON = 20.0f; constexpr int FA_PER_ROW = 10; @@ -4349,7 +4365,7 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage } // Upload to Vulkan via VkContext - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; auto* vkCtx = window ? window->getVkContext() : nullptr; if (!vkCtx) { spellIconCache_[spellId] = VK_NULL_HANDLE; @@ -4456,7 +4472,7 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { const auto& questLog = gameHandler.getQuestLog(); if (questLog.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; constexpr float TRACKER_W = 220.0f; @@ -4694,12 +4710,12 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { // Reset mouseover each frame; we'll set it below when the cursor is over a nameplate gameHandler.setMouseoverGuid(0); - auto* appRenderer = core::Application::getInstance().getRenderer(); + auto* appRenderer = services_.renderer; if (!appRenderer) return; rendering::Camera* camera = appRenderer->getCamera(); if (!camera) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (!window) return; const float screenW = static_cast(window->getWidth()); const float screenH = static_cast(window->getHeight()); @@ -4908,7 +4924,7 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { // Spell icon + name above the cast bar const std::string& spellName = gameHandler.getSpellName(cs->spellId); { - auto* castAm = core::Application::getInstance().getAssetManager(); + auto* castAm = services_.assetManager; VkDescriptorSet castIcon = (cs->spellId && castAm) ? getSpellIcon(cs->spellId, castAm) : VK_NULL_HANDLE; float iconSz = cbH + 8.0f; @@ -5301,7 +5317,7 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { // ============================================================ void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (!renderer) return; // Build path: ~/.wowee/screenshots/WoWee_YYYYMMDD_HHMMSS.png @@ -5332,7 +5348,7 @@ void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) { sysMsg.type = game::ChatType::SYSTEM; sysMsg.language = game::ChatLanguage::UNIVERSAL; sysMsg.message = "Screenshot saved: " + path; - core::Application::getInstance().getGameHandler()->addLocalChatMessage(sysMsg); + services_.gameHandler->addLocalChatMessage(sysMsg); } } @@ -5412,7 +5428,7 @@ void GameScreen::renderUIErrors(game::GameHandler& /*gameHandler*/, float deltaT if (uiErrors_.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -5551,9 +5567,9 @@ void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) { const auto& statuses = gameHandler.getNpcQuestStatuses(); if (statuses.empty()) return; - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; auto* camera = renderer ? renderer->getCamera() : nullptr; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (!camera || !window) return; float screenW = static_cast(window->getWidth()); @@ -5629,10 +5645,10 @@ void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) { void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { const auto& statuses = gameHandler.getNpcQuestStatuses(); - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; auto* camera = renderer ? renderer->getCamera() : nullptr; auto* minimap = renderer ? renderer->getMinimap() : nullptr; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; if (!camera || !minimap || !window) return; float screenW = static_cast(window->getWidth()); @@ -6509,7 +6525,7 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } auto applyMuteState = [&]() { - auto* activeRenderer = core::Application::getInstance().getRenderer(); + auto* activeRenderer = services_.renderer; float masterScale = settingsPanel_.soundMuted_ ? 0.0f : static_cast(settingsPanel_.pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(masterScale); if (!activeRenderer) return; @@ -6843,7 +6859,7 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { // Calendar pending invites indicator (WotLK only) { - auto* expReg = core::Application::getInstance().getExpansionRegistry(); + auto* expReg = services_.expansionRegistry; bool isWotLK = expReg && expReg->getActive() && expReg->getActive()->id == "wotlk"; if (isWotLK) { uint32_t calPending = gameHandler.getCalendarPendingInvites(); @@ -7197,7 +7213,7 @@ void GameScreen::loadSettings() { else if (key == "shadow_distance") settingsPanel_.pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f); else if (key == "brightness") { settingsPanel_.pendingBrightness = std::clamp(std::stoi(val), 0, 100); - if (auto* r = core::Application::getInstance().getRenderer()) + if (auto* r = services_.renderer) r->setBrightness(static_cast(settingsPanel_.pendingBrightness) / 50.0f); } else if (key == "water_refraction") settingsPanel_.pendingWaterRefraction = (std::stoi(val) != 0); @@ -7229,7 +7245,7 @@ void GameScreen::loadSettings() { else if (key == "camera_pivot_height") settingsPanel_.pendingPivotHeight = std::clamp(std::stof(val), 0.0f, 3.0f); else if (key == "fov") { settingsPanel_.pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f); - if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* renderer = services_.renderer) { if (auto* camera = renderer->getCamera()) camera->setFov(settingsPanel_.pendingFov); } } diff --git a/src/ui/settings_panel.cpp b/src/ui/settings_panel.cpp index a7676d61..15acfc13 100644 --- a/src/ui/settings_panel.cpp +++ b/src/ui/settings_panel.cpp @@ -152,7 +152,7 @@ ImGui::EndChild(); void SettingsPanel::renderSettingsGameplayTab(InventoryScreen& inventoryScreen, std::function saveCallback) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; ImGui::Spacing(); ImGui::Text("Controls"); @@ -433,7 +433,7 @@ if (ImGui::Button("Reset to Defaults", ImVec2(-1, 0))) { } void SettingsPanel::renderSettingsAudioTab(std::function saveCallback) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; ImGui::Spacing(); ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true); @@ -599,8 +599,8 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP std::function saveCallback) { if (!showSettingsWindow) return; - auto* window = core::Application::getInstance().getWindow(); - auto* renderer = core::Application::getInstance().getRenderer(); + auto* window = services_.window; + auto* renderer = services_.renderer; if (!window) return; static constexpr int kResolutions[][2] = { @@ -1045,7 +1045,7 @@ void SettingsPanel::renderSettingsWindow(InventoryScreen& inventoryScreen, ChatP } void SettingsPanel::applyGraphicsPreset(GraphicsPreset preset) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; // Define preset values based on quality level switch (preset) { diff --git a/src/ui/social_panel.cpp b/src/ui/social_panel.cpp index 6d0f0145..5a37fd8e 100644 --- a/src/ui/social_panel.cpp +++ b/src/ui/social_panel.cpp @@ -86,7 +86,7 @@ void SocialPanel::renderPartyFrames(game::GameHandler& gameHandler, SpellIconFn getSpellIcon) { if (!gameHandler.isInGroup()) return; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; const auto& partyData = gameHandler.getPartyData(); const bool isRaid = (partyData.groupType == 1); float frameY = 120.0f; @@ -117,7 +117,7 @@ void SocialPanel::renderPartyFrames(game::GameHandler& gameHandler, float winW = activeSgs * (CELL_W + CELL_PAD) + CELL_PAD + 8.0f; float winH = MAX_PER_GROUP * (CELL_H + CELL_PAD) + CELL_PAD + 20.0f; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float raidX = (screenW - winW) / 2.0f; @@ -757,7 +757,7 @@ void SocialPanel::renderPartyFrames(game::GameHandler& gameHandler, void SocialPanel::renderBossFrames(game::GameHandler& gameHandler, SpellbookScreen& spellbookScreen, SpellIconFn getSpellIcon) { - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; // Collect active boss unit slots struct BossSlot { uint32_t slot; uint64_t guid; }; @@ -1143,11 +1143,11 @@ void SocialPanel::renderGuildRoster(game::GameHandler& gameHandler, // Get zone manager for name lookup game::ZoneManager* zoneManager = nullptr; - if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* renderer = services_.renderer) { zoneManager = renderer->getZoneManager(); } - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -1683,7 +1683,7 @@ void SocialPanel::renderSocialFrame(game::GameHandler& gameHandler, for (const auto& c : contacts) if (c.isFriend() && c.isOnline()) ++onlineCount; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW - 230.0f, 240.0f), ImGuiCond_Once); @@ -1705,7 +1705,7 @@ void SocialPanel::renderSocialFrame(game::GameHandler& gameHandler, // Get zone manager for area name lookups game::ZoneManager* socialZoneMgr = nullptr; - if (auto* rend = core::Application::getInstance().getRenderer()) + if (auto* rend = services_.renderer) socialZoneMgr = rend->getZoneManager(); if (ImGui::BeginTabBar("##SocialTabs")) { @@ -2048,7 +2048,7 @@ void SocialPanel::renderDungeonFinderWindow(game::GameHandler& gameHandler, if (!showDungeonFinder_) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -2423,7 +2423,7 @@ void SocialPanel::renderInspectWindow(game::GameHandler& gameHandler, // Lazy-load SpellItemEnchantment.dbc for enchant name lookup static std::unordered_map s_enchantNames; static bool s_enchantDbLoaded = false; - auto* assetMgrEnchant = core::Application::getInstance().getAssetManager(); + auto* assetMgrEnchant = services_.assetManager; if (!s_enchantDbLoaded && assetMgrEnchant && assetMgrEnchant->isInitialized()) { s_enchantDbLoaded = true; auto dbc = assetMgrEnchant->loadDBC("SpellItemEnchantment.dbc"); diff --git a/src/ui/toast_manager.cpp b/src/ui/toast_manager.cpp index 1339b7f1..7f76c961 100644 --- a/src/ui/toast_manager.cpp +++ b/src/ui/toast_manager.cpp @@ -128,7 +128,7 @@ void ToastManager::setupCallbacks(game::GameHandler& gameHandler) { // --------------------------------------------------------------------------- void ToastManager::renderEarlyToasts(float deltaTime, game::GameHandler& gameHandler) { // Zone entry detection — fire a toast when the renderer's zone name changes - if (auto* rend = core::Application::getInstance().getRenderer()) { + if (auto* rend = services_.renderer) { const std::string& curZone = rend->getCurrentZoneName(); if (!curZone.empty() && curZone != lastKnownZone_) { if (!lastKnownZone_.empty()) { @@ -175,7 +175,7 @@ void ToastManager::renderRepToasts(float deltaTime) { if (repToasts_.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -254,7 +254,7 @@ void ToastManager::renderQuestCompleteToasts(float deltaTime) { if (questCompleteToasts_.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -329,7 +329,7 @@ void ToastManager::renderZoneToasts(float deltaTime) { if (zoneToasts_.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImDrawList* draw = ImGui::GetForegroundDrawList(); @@ -395,7 +395,7 @@ void ToastManager::renderAreaTriggerToasts(float deltaTime, game::GameHandler& g areaTriggerToasts_.end()); if (areaTriggerToasts_.empty()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -461,7 +461,7 @@ void ToastManager::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t man dingStats_[3] = intel; dingStats_[4] = spi; - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { if (auto* sfx = renderer->getUiSoundManager()) { sfx->playLevelUp(); @@ -550,7 +550,7 @@ void ToastManager::triggerAchievementToast(uint32_t achievementId, std::string n achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION; // Play a UI sound if available - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { if (auto* sfx = renderer->getUiSoundManager()) { sfx->playAchievementAlert(); @@ -565,7 +565,7 @@ void ToastManager::renderAchievementToast() { achievementToastTimer_ -= dt; if (achievementToastTimer_ < 0.0f) achievementToastTimer_ = 0.0f; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -641,7 +641,7 @@ void ToastManager::renderDiscoveryToast() { alpha = 1.0f; alpha = std::clamp(alpha, 0.0f, 1.0f); - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -1183,7 +1183,7 @@ void ToastManager::renderZoneText(game::GameHandler& gameHandler) { // Also poll the renderer for zone name changes (covers map-level transitions // where worldStateZoneId may not change immediately). - auto* appRenderer = core::Application::getInstance().getRenderer(); + auto* appRenderer = services_.renderer; if (appRenderer) { const std::string& zoneName = appRenderer->getCurrentZoneName(); if (!zoneName.empty() && zoneName != lastKnownZoneName_) { @@ -1202,7 +1202,7 @@ void ToastManager::renderZoneText(game::GameHandler& gameHandler) { zoneTextTimer_ -= dt; if (zoneTextTimer_ < 0.0f) zoneTextTimer_ = 0.0f; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; diff --git a/src/ui/window_manager.cpp b/src/ui/window_manager.cpp index 2091a8d9..c87a3f64 100644 --- a/src/ui/window_manager.cpp +++ b/src/ui/window_manager.cpp @@ -83,7 +83,7 @@ void WindowManager::renderLootWindow(game::GameHandler& gameHandler, ChatPanel& chatPanel) { if (!gameHandler.isLootWindowOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Appearing); @@ -273,7 +273,7 @@ void WindowManager::renderGossipWindow(game::GameHandler& gameHandler, ChatPanel& chatPanel) { if (!gameHandler.isGossipWindowOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing); @@ -445,7 +445,7 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen) { if (!gameHandler.isQuestDetailsOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -569,7 +569,7 @@ void WindowManager::renderQuestRequestItemsWindow(game::GameHandler& gameHandler InventoryScreen& inventoryScreen) { if (!gameHandler.isQuestRequestItemsOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -672,7 +672,7 @@ void WindowManager::renderQuestOfferRewardWindow(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen) { if (!gameHandler.isQuestOfferRewardOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -845,7 +845,7 @@ void WindowManager::renderQuestOfferRewardWindow(game::GameHandler& gameHandler, void WindowManager::loadExtendedCostDBC() { if (extendedCostDbLoaded_) return; extendedCostDbLoaded_ = true; - auto* am = core::Application::getInstance().getAssetManager(); + auto* am = services_.assetManager; if (!am || !am->isInitialized()) return; auto dbc = am->loadDBC("ItemExtendedCost.dbc"); if (!dbc || !dbc->isLoaded()) return; @@ -898,7 +898,7 @@ void WindowManager::renderVendorWindow(game::GameHandler& gameHandler, ChatPanel& chatPanel) { if (!gameHandler.isVendorWindowOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing); @@ -1236,9 +1236,9 @@ void WindowManager::renderTrainerWindow(game::GameHandler& gameHandler, SpellIconFn getSpellIcon) { if (!gameHandler.isTrainerWindowOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* assetMgr = services_.assetManager; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing); @@ -1701,7 +1701,7 @@ void WindowManager::renderEscapeMenu(SettingsPanel& settingsPanel) { settingsPanel.showEscapeSettingsNotice = false; } if (ImGui::Button("Quit", ImVec2(-1, 0))) { - auto* renderer = core::Application::getInstance().getRenderer(); + auto* renderer = services_.renderer; if (renderer) { if (auto* music = renderer->getMusicManager()) { music->stopMusic(0.0f); @@ -1763,7 +1763,7 @@ void WindowManager::renderBarberShopWindow(game::GameHandler& gameHandler) { int maxHairColor = static_cast(game::getMaxHairColor(raceEnum, gender)); int maxFacialHair = static_cast(game::getMaxFacialFeature(raceEnum, gender)); - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float winW = 300.0f; @@ -1847,7 +1847,7 @@ void WindowManager::renderBarberShopWindow(game::GameHandler& gameHandler) { void WindowManager::renderStableWindow(game::GameHandler& gameHandler) { if (!gameHandler.isStableWindowOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -1960,7 +1960,7 @@ void WindowManager::renderStableWindow(game::GameHandler& gameHandler) { void WindowManager::renderTaxiWindow(game::GameHandler& gameHandler) { if (!gameHandler.isTaxiWindowOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing); @@ -2058,7 +2058,7 @@ void WindowManager::renderTaxiWindow(game::GameHandler& gameHandler) { void WindowManager::renderLogoutCountdown(game::GameHandler& gameHandler) { if (!gameHandler.isLoggingOut()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -2127,7 +2127,7 @@ void WindowManager::renderDeathScreen(game::GameHandler& gameHandler) { deathElapsed_ += dt; } - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -2216,7 +2216,7 @@ void WindowManager::renderDeathScreen(game::GameHandler& gameHandler) { void WindowManager::renderReclaimCorpseButton(game::GameHandler& gameHandler) { if (!gameHandler.isPlayerGhost() || !gameHandler.canReclaimCorpse()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -2274,7 +2274,7 @@ void WindowManager::renderMailWindow(game::GameHandler& gameHandler, ChatPanel& chatPanel) { if (!gameHandler.isMailboxOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 250, 80), ImGuiCond_Appearing); @@ -2553,7 +2553,7 @@ void WindowManager::renderMailComposeWindow(game::GameHandler& gameHandler, InventoryScreen& inventoryScreen) { if (!gameHandler.isMailComposeOpen()) return; - auto* window = core::Application::getInstance().getWindow(); + auto* window = services_.window; float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; @@ -3769,7 +3769,7 @@ void WindowManager::renderAchievementWindow(game::GameHandler& gameHandler) { static bool s_criteriaDataLoaded = false; if (!s_criteriaDataLoaded) { s_criteriaDataLoaded = true; - auto* am = core::Application::getInstance().getAssetManager(); + auto* am = services_.assetManager; if (am && am->isInitialized()) { auto dbc = am->loadDBC("AchievementCriteria.dbc"); if (dbc && dbc->isLoaded() && dbc->getFieldCount() >= 10) {