chore(application): refactor app lifecycle and add entity spawner module

- updated CMakeLists.txt to include new module targets
- refactored application.hpp + application.cpp
- added new `entity_spawner` headers + sources:
  - entity_spawner.hpp
  - entity_spawner.cpp
This commit is contained in:
Paul 2026-03-31 22:01:55 +03:00
parent b6e4e405b6
commit cf3ae3bbfe
5 changed files with 5641 additions and 5310 deletions

View file

@ -486,6 +486,7 @@ endif()
set(WOWEE_SOURCES set(WOWEE_SOURCES
# Core # Core
src/core/application.cpp src/core/application.cpp
src/core/entity_spawner.cpp
src/core/window.cpp src/core/window.cpp
src/core/input.cpp src/core/input.cpp
src/core/logger.cpp src/core/logger.cpp

View file

@ -2,6 +2,7 @@
#include "core/window.hpp" #include "core/window.hpp"
#include "core/input.hpp" #include "core/input.hpp"
#include "core/entity_spawner.hpp"
#include "game/character.hpp" #include "game/character.hpp"
#include "game/game_services.hpp" #include "game/game_services.hpp"
#include "pipeline/blp_loader.hpp" #include "pipeline/blp_loader.hpp"
@ -79,7 +80,7 @@ public:
// Logout to login screen // Logout to login screen
void logoutToLogin(); void logoutToLogin();
// Render bounds lookup (for click targeting / selection) // Render bounds lookup (for click targeting / selection) — delegates to EntitySpawner
bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const; bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const;
bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const; bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const;
bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const; bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const;
@ -89,8 +90,11 @@ public:
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; } const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; } uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; }
uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; } uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; }
uint32_t getGryphonDisplayId() const { return gryphonDisplayId_; } uint32_t getGryphonDisplayId() const { return entitySpawner_ ? entitySpawner_->getGryphonDisplayId() : 0; }
uint32_t getWyvernDisplayId() const { return wyvernDisplayId_; } uint32_t getWyvernDisplayId() const { return entitySpawner_ ? entitySpawner_->getWyvernDisplayId() : 0; }
// Entity spawner access
EntitySpawner* getEntitySpawner() { return entitySpawner_.get(); }
private: private:
void update(float deltaTime); void update(float deltaTime);
@ -102,27 +106,6 @@ private:
static const char* mapDisplayName(uint32_t mapId); static const char* mapDisplayName(uint32_t mapId);
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
void buildFactionHostilityMap(uint8_t playerRace); void buildFactionHostilityMap(uint8_t playerRace);
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale = 1.0f);
void despawnOnlineCreature(uint64_t guid);
bool tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId);
void spawnOnlinePlayer(uint64_t guid,
uint8_t raceId,
uint8_t genderId,
uint32_t appearanceBytes,
uint8_t facialFeatures,
float x, float y, float z, float orientation);
void setOnlinePlayerEquipment(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes);
void despawnOnlinePlayer(uint64_t guid);
void buildCreatureDisplayLookups();
std::string getModelPathForDisplayId(uint32_t displayId) const;
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale = 1.0f);
void despawnOnlineGameObject(uint64_t guid);
void buildGameObjectDisplayLookups();
std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const;
audio::VoiceType detectVoiceTypeFromDisplayId(uint32_t displayId) const;
void setupTestTransport(); // Test transport boat for development void setupTestTransport(); // Test transport boat for development
static Application* instance; static Application* instance;
@ -139,6 +122,7 @@ private:
bool addonsLoaded_ = false; bool addonsLoaded_ = false;
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_; std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
std::unique_ptr<pipeline::DBCLayout> dbcLayout_; std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
std::unique_ptr<EntitySpawner> entitySpawner_;
AppState state = AppState::AUTHENTICATION; AppState state = AppState::AUTHENTICATION;
bool running = false; bool running = false;
@ -156,94 +140,12 @@ private:
uint32_t spawnedAppearanceBytes_ = 0; uint32_t spawnedAppearanceBytes_ = 0;
uint8_t spawnedFacialFeatures_ = 0; uint8_t spawnedFacialFeatures_ = 0;
// Weapon model ID counter (starting high to avoid collision with character model IDs)
uint32_t nextWeaponModelId_ = 1000;
// Saved at spawn for skin re-compositing // Saved at spawn for skin re-compositing
std::string bodySkinPath_; std::string bodySkinPath_;
std::vector<std::string> underwearPaths_; std::vector<std::string> underwearPaths_;
uint32_t skinTextureSlotIndex_ = 0; uint32_t skinTextureSlotIndex_ = 0;
uint32_t cloakTextureSlotIndex_ = 0; uint32_t cloakTextureSlotIndex_ = 0;
// Online creature model spawning
struct CreatureDisplayData {
uint32_t modelId = 0;
std::string skin1, skin2, skin3; // Texture names from CreatureDisplayInfo.dbc
uint32_t extraDisplayId = 0; // Link to CreatureDisplayInfoExtra.dbc
};
struct HumanoidDisplayExtra {
uint8_t raceId = 0;
uint8_t sexId = 0;
uint8_t skinId = 0;
uint8_t faceId = 0;
uint8_t hairStyleId = 0;
uint8_t hairColorId = 0;
uint8_t facialHairId = 0;
std::string bakeName; // Pre-baked texture path if available
// Equipment display IDs (from columns 8-18)
// 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
uint32_t equipDisplayId[11] = {0};
};
std::unordered_map<uint32_t, CreatureDisplayData> displayDataMap_; // displayId → display data
std::unordered_map<uint32_t, HumanoidDisplayExtra> humanoidExtraMap_; // extraDisplayId → humanoid data
std::unordered_map<uint32_t, std::string> modelIdToPath_; // modelId → M2 path (from CreatureModelData.dbc)
// CharHairGeosets.dbc: key = (raceId<<16)|(sexId<<8)|variationId → geosetId (skinSectionId)
std::unordered_map<uint32_t, uint16_t> hairGeosetMap_;
// CharFacialHairStyles.dbc: key = (raceId<<16)|(sexId<<8)|variationId → {geoset100, geoset300, geoset200}
struct FacialHairGeosets { uint16_t geoset100 = 0; uint16_t geoset300 = 0; uint16_t geoset200 = 0; };
std::unordered_map<uint32_t, FacialHairGeosets> facialHairGeosetMap_;
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
std::unordered_map<uint64_t, bool> creatureWasMoving_; // guid -> previous-frame movement state
std::unordered_map<uint64_t, bool> creatureWasSwimming_; // guid -> previous-frame swim state (for anim transition detection)
std::unordered_map<uint64_t, bool> creatureWasFlying_; // guid -> previous-frame flying state (for anim transition detection)
std::unordered_map<uint64_t, bool> creatureWasWalking_; // guid -> previous-frame walking state (walk vs run transition detection)
std::unordered_map<uint64_t, bool> creatureSwimmingState_; // guid -> currently in swim mode (SWIMMING flag)
std::unordered_map<uint64_t, bool> creatureWalkingState_; // guid -> walking (WALKING flag, selects Walk(4) vs Run(5))
std::unordered_map<uint64_t, bool> creatureFlyingState_; // guid -> currently flying (FLYING flag)
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check
static constexpr int MAX_WEAPON_ATTACHES_PER_TICK = 2; // limit weapon attach work per 1s tick
// CharSections.dbc lookup cache to avoid O(N) DBC scan per NPC spawn.
// Key: (race<<24)|(sex<<16)|(section<<12)|(variation<<8)|color → texture path
std::unordered_map<uint64_t, std::string> charSectionsCache_;
bool charSectionsCacheBuilt_ = false;
void buildCharSectionsCache();
std::string lookupCharSection(uint8_t race, uint8_t sex, uint8_t section,
uint8_t variation, uint8_t color, int texIndex = 0) const;
// Async creature model loading: file I/O + M2 parsing on background thread,
// GPU upload + instance creation on main thread.
struct PreparedCreatureModel {
uint64_t guid;
uint32_t displayId;
uint32_t modelId;
float x, y, z, orientation;
float scale = 1.0f;
std::shared_ptr<pipeline::M2Model> model; // parsed on background thread
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures; // decoded on bg thread
bool valid = false;
bool permanent_failure = false;
};
struct AsyncCreatureLoad {
std::future<PreparedCreatureModel> future;
};
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
std::unordered_set<uint32_t> asyncCreatureDisplayLoads_; // displayIds currently loading in background
void processAsyncCreatureResults(bool unlimited = false);
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
std::unordered_set<uint32_t> displayIdTexturesApplied_; // displayIds with per-model textures applied
std::unordered_map<uint32_t, std::unordered_map<std::string, pipeline::BLPImage>> displayIdPredecodedTextures_; // displayId → pre-decoded skin textures
mutable std::unordered_set<uint32_t> warnedMissingDisplayDataIds_; // displayIds already warned
mutable std::unordered_set<uint32_t> warnedMissingModelPathIds_; // modelIds/displayIds already warned
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
uint32_t gryphonDisplayId_ = 0;
uint32_t wyvernDisplayId_ = 0;
bool lastTaxiFlight_ = false; bool lastTaxiFlight_ = false;
uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none) 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 uint32_t worldLoadGeneration_ = 0; // Incremented on each world entry to detect re-entrant loads
@ -272,190 +174,11 @@ private:
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
uint64_t chargeTargetGuid_ = 0; uint64_t chargeTargetGuid_ = 0;
// Online gameobject model spawning
struct GameObjectInstanceInfo {
uint32_t modelId = 0;
uint32_t instanceId = 0;
bool isWmo = false;
};
std::unordered_map<uint32_t, std::string> gameObjectDisplayIdToPath_;
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdModelCache_; // displayId → M2 modelId
std::unordered_set<uint32_t> gameObjectDisplayIdFailedCache_; // displayIds that permanently fail to load
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdWmoCache_; // displayId → WMO modelId
std::unordered_map<uint64_t, GameObjectInstanceInfo> gameObjectInstances_; // guid → instance info
struct PendingTransportMove {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float orientation = 0.0f;
};
struct PendingTransportRegistration {
uint64_t guid = 0;
uint32_t entry = 0;
uint32_t displayId = 0;
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float orientation = 0.0f;
};
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_; // guid -> latest pre-registration move
std::deque<PendingTransportRegistration> pendingTransportRegistrations_;
uint32_t nextGameObjectModelId_ = 20000;
uint32_t nextGameObjectWmoModelId_ = 40000;
bool testTransportSetup_ = false;
bool gameObjectLookupsBuilt_ = false;
// Mount model tracking
uint32_t mountInstanceId_ = 0;
uint32_t mountModelId_ = 0;
uint32_t pendingMountDisplayId_ = 0; // Deferred mount load (0 = none pending)
bool weaponsSheathed_ = false; bool weaponsSheathed_ = false;
bool wasAutoAttacking_ = false; bool wasAutoAttacking_ = false;
void processPendingMount();
bool creatureLookupsBuilt_ = false;
bool mapNameCacheLoaded_ = false; bool mapNameCacheLoaded_ = false;
std::unordered_map<uint32_t, std::string> mapNameById_; std::unordered_map<uint32_t, std::string> mapNameById_;
// Deferred creature spawn queue (throttles spawning to avoid hangs)
struct PendingCreatureSpawn {
uint64_t guid;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
};
std::deque<PendingCreatureSpawn> pendingCreatureSpawns_;
static constexpr int MAX_SPAWNS_PER_FRAME = 3;
static constexpr int MAX_NEW_CREATURE_MODELS_PER_FRAME = 1;
static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300;
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
std::unordered_set<uint32_t> nonRenderableCreatureDisplayIds_;
// Online player instances (separate from creatures so we can apply per-player skin/hair textures).
std::unordered_map<uint64_t, uint32_t> playerInstances_; // guid → render instanceId
struct OnlinePlayerAppearanceState {
uint32_t instanceId = 0;
uint32_t modelId = 0;
uint8_t raceId = 0;
uint8_t genderId = 0;
uint32_t appearanceBytes = 0;
uint8_t facialFeatures = 0;
std::string bodySkinPath;
std::vector<std::string> underwearPaths;
};
std::unordered_map<uint64_t, OnlinePlayerAppearanceState> onlinePlayerAppearance_;
std::unordered_map<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>> pendingOnlinePlayerEquipment_;
// Deferred equipment compositing queue — processes max 1 per frame to avoid stutter
// deque: consumed from front in a loop; vector::erase(begin) would be O(n²).
std::deque<std::pair<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>>> deferredEquipmentQueue_;
void processDeferredEquipmentQueue();
// Async equipment texture pre-decode: BLP decode on background thread, composite on main thread
struct PreparedEquipmentUpdate {
uint64_t guid;
std::array<uint32_t, 19> displayInfoIds;
std::array<uint8_t, 19> inventoryTypes;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncEquipmentLoad {
std::future<PreparedEquipmentUpdate> future;
};
std::vector<AsyncEquipmentLoad> asyncEquipmentLoads_;
void processAsyncEquipmentResults();
std::vector<std::string> resolveEquipmentTexturePaths(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes) const;
// Deferred NPC texture setup — async DBC lookups + BLP pre-decode to avoid main-thread stalls
struct DeferredNpcComposite {
uint32_t modelId;
uint32_t displayId;
// Skin compositing (type-1 slots)
std::string basePath; // CharSections skin base texture
std::vector<std::string> overlayPaths; // face + underwear overlays
std::vector<std::pair<int, std::string>> regionLayers; // equipment region overlays
std::vector<uint32_t> skinTextureSlots; // model texture slots needing skin composite
bool hasComposite = false; // needs compositing (overlays or equipment regions)
bool hasSimpleSkin = false; // just base skin, no compositing needed
// Baked skin (type-1 slots)
std::string bakedSkinPath; // baked texture path (if available)
bool hasBakedSkin = false; // baked skin resolved successfully
// Hair (type-6 slots)
std::vector<uint32_t> hairTextureSlots; // model texture slots needing hair texture
std::string hairTexturePath; // resolved hair texture path
bool useBakedForHair = false; // bald NPC: use baked skin for type-6
};
struct PreparedNpcComposite {
DeferredNpcComposite info;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncNpcCompositeLoad {
std::future<PreparedNpcComposite> future;
};
std::vector<AsyncNpcCompositeLoad> asyncNpcCompositeLoads_;
void processAsyncNpcCompositeResults(bool unlimited = false);
// Cache base player model geometry by (raceId, genderId)
std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };
std::unordered_map<uint32_t, PlayerTextureSlots> playerTextureSlotsByModelId_;
uint32_t nextPlayerModelId_ = 60000;
struct PendingPlayerSpawn {
uint64_t guid;
uint8_t raceId;
uint8_t genderId;
uint32_t appearanceBytes;
uint8_t facialFeatures;
float x, y, z, orientation;
};
std::deque<PendingPlayerSpawn> pendingPlayerSpawns_;
std::unordered_set<uint64_t> pendingPlayerSpawnGuids_;
void processPlayerSpawnQueue();
std::unordered_set<uint64_t> creaturePermanentFailureGuids_;
void processCreatureSpawnQueue(bool unlimited = false);
struct PendingGameObjectSpawn {
uint64_t guid;
uint32_t entry;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
};
std::deque<PendingGameObjectSpawn> pendingGameObjectSpawns_;
void processGameObjectSpawnQueue();
// Async WMO loading for game objects (file I/O + parse on background thread)
struct PreparedGameObjectWMO {
uint64_t guid;
uint32_t entry;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
std::shared_ptr<pipeline::WMOModel> wmoModel;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures; // decoded on bg thread
bool valid = false;
bool isWmo = false;
std::string modelPath;
};
struct AsyncGameObjectLoad {
std::future<PreparedGameObjectWMO> future;
};
std::vector<AsyncGameObjectLoad> asyncGameObjectLoads_;
void processAsyncGameObjectResults();
struct PendingTransportDoodadBatch {
uint64_t guid = 0;
uint32_t modelId = 0;
uint32_t instanceId = 0;
size_t nextIndex = 0;
size_t doodadBudget = 0;
size_t spawnedDoodads = 0;
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float orientation = 0.0f;
};
std::vector<PendingTransportDoodadBatch> pendingTransportDoodadBatches_;
static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4;
void processPendingTransportRegistrations();
void processPendingTransportDoodads();
// Quest marker billboard sprites (above NPCs) // Quest marker billboard sprites (above NPCs)
void loadQuestMarkerModels(); // Now loads BLP textures void loadQuestMarkerModels(); // Now loads BLP textures
void updateQuestMarkers(); // Updates billboard positions void updateQuestMarkers(); // Updates billboard positions

View file

@ -0,0 +1,465 @@
#pragma once
#include "game/character.hpp"
#include "game/game_services.hpp"
#include "pipeline/blp_loader.hpp"
#include <memory>
#include <string>
#include <vector>
#include <deque>
#include <unordered_map>
#include <unordered_set>
#include <array>
#include <optional>
#include <future>
#include <mutex>
#include <glm/glm.hpp>
namespace wowee {
// Forward declarations
namespace rendering { class Renderer; }
namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; struct WMOModel; }
namespace audio { enum class VoiceType; }
namespace game { class GameHandler; }
namespace core {
class Application;
class EntitySpawner {
public:
EntitySpawner(rendering::Renderer* renderer,
pipeline::AssetManager* assetManager,
game::GameHandler* gameHandler,
pipeline::DBCLayout* dbcLayout,
game::GameServices* gameServices);
~EntitySpawner();
EntitySpawner(const EntitySpawner&) = delete;
EntitySpawner& operator=(const EntitySpawner&) = delete;
// Lifecycle
void initialize(); // Build DBC lookups
void update(); // Process all spawn/despawn queues (called from Application::update)
void shutdown(); // Clear all instances and queues
// Queue-based spawn API (called from GameHandler callbacks)
void queueCreatureSpawn(uint64_t guid, uint32_t displayId,
float x, float y, float z, float orientation, float scale = 1.0f);
void queuePlayerSpawn(uint64_t guid, uint8_t raceId, uint8_t genderId,
uint32_t appearanceBytes, uint8_t facialFeatures,
float x, float y, float z, float orientation);
void queueGameObjectSpawn(uint64_t guid, uint32_t entry, uint32_t displayId,
float x, float y, float z, float orientation, float scale = 1.0f);
void queuePlayerEquipment(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes);
// Immediate despawn
void despawnCreature(uint64_t guid);
void despawnPlayer(uint64_t guid);
void despawnGameObject(uint64_t guid);
// Clear all queues and instances (logout, reconnect)
void clearAllQueues();
void despawnAllCreatures();
void despawnAllPlayers();
void despawnAllGameObjects();
// Full reset — despawns entities, waits for async loads, clears all state.
// Used by logoutToLogin() and loadOnlineWorldTerrain() for clean slate.
void resetAllState();
// Rebuild lookup tables (for reloadExpansionData after expansion change)
void rebuildLookups();
// Check if any spawn/async work is still pending
bool hasWorkPending() const;
// Status queries
bool isCreatureSpawned(uint64_t guid) const { return creatureInstances_.count(guid) > 0; }
bool isCreaturePending(uint64_t guid) const { return pendingCreatureSpawnGuids_.count(guid) > 0; }
bool isPlayerSpawned(uint64_t guid) const { return playerInstances_.count(guid) > 0; }
bool isPlayerPending(uint64_t guid) const { return pendingPlayerSpawnGuids_.count(guid) > 0; }
bool isGameObjectSpawned(uint64_t guid) const { return gameObjectInstances_.count(guid) > 0; }
// Quick instance ID lookups (returns 0 if not found)
uint32_t getCreatureInstanceId(uint64_t guid) const {
auto it = creatureInstances_.find(guid); return (it != creatureInstances_.end()) ? it->second : 0;
}
uint32_t getPlayerInstanceId(uint64_t guid) const {
auto it = playerInstances_.find(guid); return (it != playerInstances_.end()) ? it->second : 0;
}
// Render bounds/position queries (used by click targeting, etc.)
bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const;
bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const;
bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const;
// Display data lookups
bool areCreatureLookupsBuilt() const { return creatureLookupsBuilt_; }
bool areGameObjectLookupsBuilt() const { return gameObjectLookupsBuilt_; }
std::string getModelPathForDisplayId(uint32_t displayId) const;
std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const;
audio::VoiceType detectVoiceTypeFromDisplayId(uint32_t displayId) const;
// Weapon attachment for NPC virtual weapons
bool tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId);
// Mount
void setMountDisplayId(uint32_t displayId) { pendingMountDisplayId_ = displayId; }
uint32_t getMountInstanceId() const { return mountInstanceId_; }
uint32_t getMountModelId() const { return mountModelId_; }
// Local player GUID exclusion (EntitySpawner skips spawning the local player)
void setLocalPlayerGuid(uint64_t guid) { spawnedPlayerGuid_ = guid; }
// Weapon model IDs (shared counter for creature weapons + equipment helm/weapon models)
uint32_t allocateWeaponModelId() { return nextWeaponModelId_++; }
// Maximum weapon attachment operations per tick (used by Application update loop)
static constexpr int MAX_WEAPON_ATTACHES_PER_TICK = 2;
// Dead creature tracking (spawn in corpse/death pose)
void markCreatureDead(uint64_t guid) { deadCreatureGuids_.insert(guid); }
void unmarkCreatureDead(uint64_t guid) { deadCreatureGuids_.erase(guid); }
void clearDeadCreatureGuids() { deadCreatureGuids_.clear(); }
// Mount state management
void clearMountState();
// Transport registration API (used by setupUICallbacks transport spawn callback)
void queueTransportRegistration(uint64_t guid, uint32_t entry, uint32_t displayId,
float x, float y, float z, float orientation);
void setTransportPendingMove(uint64_t guid, float x, float y, float z, float orientation);
bool hasTransportRegistrationPending(uint64_t guid) const;
void updateTransportRegistration(uint64_t guid, uint32_t displayId,
float x, float y, float z, float orientation);
// Test transport setup flag
bool isTestTransportSetup() const { return testTransportSetup_; }
void setTestTransportSetup(bool v) { testTransportSetup_ = v; }
// Creature animation state accessors (for movement sync in Application::update)
std::unordered_map<uint64_t, uint32_t>& getCreatureInstances() { return creatureInstances_; }
const std::unordered_map<uint64_t, uint32_t>& getCreatureInstances() const { return creatureInstances_; }
std::unordered_map<uint64_t, uint32_t>& getCreatureModelIds() { return creatureModelIds_; }
std::unordered_map<uint64_t, glm::vec3>& getCreatureRenderPosCache() { return creatureRenderPosCache_; }
std::unordered_map<uint64_t, bool>& getCreatureWasMoving() { return creatureWasMoving_; }
std::unordered_map<uint64_t, bool>& getCreatureWasSwimming() { return creatureWasSwimming_; }
std::unordered_map<uint64_t, bool>& getCreatureWasFlying() { return creatureWasFlying_; }
std::unordered_map<uint64_t, bool>& getCreatureWasWalking() { return creatureWasWalking_; }
std::unordered_map<uint64_t, bool>& getCreatureSwimmingState() { return creatureSwimmingState_; }
std::unordered_map<uint64_t, bool>& getCreatureWalkingState() { return creatureWalkingState_; }
std::unordered_map<uint64_t, bool>& getCreatureFlyingState() { return creatureFlyingState_; }
std::unordered_set<uint64_t>& getCreatureWeaponsAttached() { return creatureWeaponsAttached_; }
std::unordered_map<uint64_t, uint8_t>& getCreatureWeaponAttachAttempts() { return creatureWeaponAttachAttempts_; }
std::unordered_map<uint32_t, bool>& getModelIdIsWolfLike() { return modelIdIsWolfLike_; }
// Player instance accessors (for movement sync in Application::update)
std::unordered_map<uint64_t, uint32_t>& getPlayerInstances() { return playerInstances_; }
const std::unordered_map<uint64_t, uint32_t>& getPlayerInstances() const { return playerInstances_; }
// GameObject instance accessors
auto& getGameObjectInstances() { return gameObjectInstances_; }
const auto& getGameObjectInstances() const { return gameObjectInstances_; }
// Display data accessors (needed by Application for gryphon/wyvern display IDs)
const auto& getDisplayDataMap() const { return displayDataMap_; }
uint32_t getGryphonDisplayId() const { return gryphonDisplayId_; }
uint32_t getWyvernDisplayId() const { return wyvernDisplayId_; }
// Character section lookups (needed for player character skin compositing)
std::string lookupCharSection(uint8_t race, uint8_t sex, uint8_t section,
uint8_t variation, uint8_t color, int texIndex = 0) const;
const std::unordered_map<uint32_t, uint16_t>& getHairGeosetMap() const { return hairGeosetMap_; }
struct FacialHairGeosets { uint16_t geoset100 = 0; uint16_t geoset300 = 0; uint16_t geoset200 = 0; };
const std::unordered_map<uint32_t, FacialHairGeosets>& getFacialHairGeosetMap() const { return facialHairGeosetMap_; }
// Creature M2 sync loader (used by spawnPlayerCharacter in Application)
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
private:
// Dependencies (non-owning)
rendering::Renderer* renderer_;
pipeline::AssetManager* assetManager_;
game::GameHandler* gameHandler_;
pipeline::DBCLayout* dbcLayout_;
game::GameServices* gameServices_;
// --- Creature display data (from DBC files) ---
struct CreatureDisplayData {
uint32_t modelId = 0;
std::string skin1, skin2, skin3; // Texture names from CreatureDisplayInfo.dbc
uint32_t extraDisplayId = 0; // Link to CreatureDisplayInfoExtra.dbc
};
struct HumanoidDisplayExtra {
uint8_t raceId = 0;
uint8_t sexId = 0;
uint8_t skinId = 0;
uint8_t faceId = 0;
uint8_t hairStyleId = 0;
uint8_t hairColorId = 0;
uint8_t facialHairId = 0;
std::string bakeName; // Pre-baked texture path if available
// Equipment display IDs (from columns 8-18)
// 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
uint32_t equipDisplayId[11] = {0};
};
std::unordered_map<uint32_t, CreatureDisplayData> displayDataMap_; // displayId → display data
std::unordered_map<uint32_t, HumanoidDisplayExtra> humanoidExtraMap_; // extraDisplayId → humanoid data
std::unordered_map<uint32_t, std::string> modelIdToPath_; // modelId → M2 path (from CreatureModelData.dbc)
// CharHairGeosets.dbc: key = (raceId<<16)|(sexId<<8)|variationId → geosetId (skinSectionId)
std::unordered_map<uint32_t, uint16_t> hairGeosetMap_;
// CharFacialHairStyles.dbc: key = (raceId<<16)|(sexId<<8)|variationId → {geoset100, geoset300, geoset200}
std::unordered_map<uint32_t, FacialHairGeosets> facialHairGeosetMap_;
bool creatureLookupsBuilt_ = false;
// CharSections.dbc lookup cache
std::unordered_map<uint64_t, std::string> charSectionsCache_;
bool charSectionsCacheBuilt_ = false;
void buildCharSectionsCache();
// Creature display lookup building
void buildCreatureDisplayLookups();
// Weapon M2 loading helper
bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel);
// GameObject display lookups
std::unordered_map<uint32_t, std::string> gameObjectDisplayIdToPath_;
bool gameObjectLookupsBuilt_ = false;
void buildGameObjectDisplayLookups();
// --- Creature instances and tracking ---
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid → last synced render position
std::unordered_map<uint64_t, bool> creatureWasMoving_;
std::unordered_map<uint64_t, bool> creatureWasSwimming_;
std::unordered_map<uint64_t, bool> creatureWasFlying_;
std::unordered_map<uint64_t, bool> creatureWasWalking_;
std::unordered_map<uint64_t, bool> creatureSwimmingState_;
std::unordered_map<uint64_t, bool> creatureWalkingState_;
std::unordered_map<uint64_t, bool> creatureFlyingState_;
std::unordered_set<uint64_t> creatureWeaponsAttached_;
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_;
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_;
// --- Creature async loads ---
struct PreparedCreatureModel {
uint64_t guid;
uint32_t displayId;
uint32_t modelId;
float x, y, z, orientation;
float scale = 1.0f;
std::shared_ptr<pipeline::M2Model> model;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
bool valid = false;
bool permanent_failure = false;
};
struct AsyncCreatureLoad {
std::future<PreparedCreatureModel> future;
};
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
std::unordered_set<uint32_t> asyncCreatureDisplayLoads_;
void processAsyncCreatureResults(bool unlimited = false);
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4;
std::unordered_set<uint64_t> deadCreatureGuids_;
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_;
std::unordered_set<uint32_t> displayIdTexturesApplied_;
std::unordered_map<uint32_t, std::unordered_map<std::string, pipeline::BLPImage>> displayIdPredecodedTextures_;
mutable std::unordered_set<uint32_t> warnedMissingDisplayDataIds_;
mutable std::unordered_set<uint32_t> warnedMissingModelPathIds_;
uint32_t nextCreatureModelId_ = 5000;
uint32_t gryphonDisplayId_ = 0;
uint32_t wyvernDisplayId_ = 0;
// --- Creature spawn queue ---
struct PendingCreatureSpawn {
uint64_t guid;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
};
std::deque<PendingCreatureSpawn> pendingCreatureSpawns_;
static constexpr int MAX_SPAWNS_PER_FRAME = 3;
static constexpr int MAX_NEW_CREATURE_MODELS_PER_FRAME = 1;
static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300;
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
std::unordered_set<uint32_t> nonRenderableCreatureDisplayIds_;
std::unordered_set<uint64_t> creaturePermanentFailureGuids_;
void processCreatureSpawnQueue(bool unlimited = false);
// --- NPC composite loads ---
struct DeferredNpcComposite {
uint32_t modelId;
uint32_t displayId;
std::string basePath;
std::vector<std::string> overlayPaths;
std::vector<std::pair<int, std::string>> regionLayers;
std::vector<uint32_t> skinTextureSlots;
bool hasComposite = false;
bool hasSimpleSkin = false;
std::string bakedSkinPath;
bool hasBakedSkin = false;
std::vector<uint32_t> hairTextureSlots;
std::string hairTexturePath;
bool useBakedForHair = false;
};
struct PreparedNpcComposite {
DeferredNpcComposite info;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncNpcCompositeLoad {
std::future<PreparedNpcComposite> future;
};
std::vector<AsyncNpcCompositeLoad> asyncNpcCompositeLoads_;
void processAsyncNpcCompositeResults(bool unlimited = false);
// --- Player instances ---
std::unordered_map<uint64_t, uint32_t> playerInstances_; // guid → render instanceId
struct OnlinePlayerAppearanceState {
uint32_t instanceId = 0;
uint32_t modelId = 0;
uint8_t raceId = 0;
uint8_t genderId = 0;
uint32_t appearanceBytes = 0;
uint8_t facialFeatures = 0;
std::string bodySkinPath;
std::vector<std::string> underwearPaths;
};
std::unordered_map<uint64_t, OnlinePlayerAppearanceState> onlinePlayerAppearance_;
std::unordered_map<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>> pendingOnlinePlayerEquipment_;
std::deque<std::pair<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>>> deferredEquipmentQueue_;
void processDeferredEquipmentQueue();
struct PreparedEquipmentUpdate {
uint64_t guid;
std::array<uint32_t, 19> displayInfoIds;
std::array<uint8_t, 19> inventoryTypes;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncEquipmentLoad {
std::future<PreparedEquipmentUpdate> future;
};
std::vector<AsyncEquipmentLoad> asyncEquipmentLoads_;
void processAsyncEquipmentResults();
std::vector<std::string> resolveEquipmentTexturePaths(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes) const;
std::unordered_map<uint32_t, uint32_t> playerModelCache_;
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };
std::unordered_map<uint32_t, PlayerTextureSlots> playerTextureSlotsByModelId_;
uint32_t nextPlayerModelId_ = 60000;
// --- Player spawn queue ---
struct PendingPlayerSpawn {
uint64_t guid;
uint8_t raceId;
uint8_t genderId;
uint32_t appearanceBytes;
uint8_t facialFeatures;
float x, y, z, orientation;
};
std::deque<PendingPlayerSpawn> pendingPlayerSpawns_;
std::unordered_set<uint64_t> pendingPlayerSpawnGuids_;
void processPlayerSpawnQueue();
// --- GameObject instances ---
struct GameObjectInstanceInfo {
uint32_t modelId = 0;
uint32_t instanceId = 0;
bool isWmo = false;
};
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdModelCache_;
std::unordered_set<uint32_t> gameObjectDisplayIdFailedCache_;
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdWmoCache_;
std::unordered_map<uint64_t, GameObjectInstanceInfo> gameObjectInstances_;
struct PendingTransportMove {
float x = 0.0f, y = 0.0f, z = 0.0f, orientation = 0.0f;
};
struct PendingTransportRegistration {
uint64_t guid = 0;
uint32_t entry = 0;
uint32_t displayId = 0;
float x = 0.0f, y = 0.0f, z = 0.0f, orientation = 0.0f;
};
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_;
std::deque<PendingTransportRegistration> pendingTransportRegistrations_;
uint32_t nextGameObjectModelId_ = 20000;
uint32_t nextGameObjectWmoModelId_ = 40000;
bool testTransportSetup_ = false;
// --- GameObject spawn queue ---
struct PendingGameObjectSpawn {
uint64_t guid;
uint32_t entry;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
};
std::deque<PendingGameObjectSpawn> pendingGameObjectSpawns_;
void processGameObjectSpawnQueue();
// --- Async WMO loading for game objects ---
struct PreparedGameObjectWMO {
uint64_t guid;
uint32_t entry;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
std::shared_ptr<pipeline::WMOModel> wmoModel;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
bool valid = false;
bool isWmo = false;
std::string modelPath;
};
struct AsyncGameObjectLoad {
std::future<PreparedGameObjectWMO> future;
};
std::vector<AsyncGameObjectLoad> asyncGameObjectLoads_;
void processAsyncGameObjectResults();
struct PendingTransportDoodadBatch {
uint64_t guid = 0;
uint32_t modelId = 0;
uint32_t instanceId = 0;
size_t nextIndex = 0;
size_t doodadBudget = 0;
size_t spawnedDoodads = 0;
float x = 0.0f, y = 0.0f, z = 0.0f, orientation = 0.0f;
};
std::vector<PendingTransportDoodadBatch> pendingTransportDoodadBatches_;
static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4;
void processPendingTransportRegistrations();
void processPendingTransportDoodads();
// --- Mount ---
uint32_t mountInstanceId_ = 0;
uint32_t mountModelId_ = 0;
uint32_t pendingMountDisplayId_ = 0;
void processPendingMount();
// --- Local player GUID exclusion ---
uint64_t spawnedPlayerGuid_ = 0;
// --- Weapon model ID counter ---
uint32_t nextWeaponModelId_ = 1000;
// --- Spawn internal methods ---
void spawnOnlineCreature(uint64_t guid, uint32_t displayId,
float x, float y, float z, float orientation, float scale = 1.0f);
void spawnOnlinePlayer(uint64_t guid, uint8_t raceId, uint8_t genderId,
uint32_t appearanceBytes, uint8_t facialFeatures,
float x, float y, float z, float orientation);
void setOnlinePlayerEquipment(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes);
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId,
float x, float y, float z, float orientation, float scale = 1.0f);
};
} // namespace core
} // namespace wowee

File diff suppressed because it is too large Load diff

4979
src/core/entity_spawner.cpp Normal file

File diff suppressed because it is too large Load diff