From d95abfb607d4fab2bc16fac6a1bbae1f9e98728a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 22:45:47 -0700 Subject: [PATCH] feat: propagate OBJECT_FIELD_SCALE_X through creature and GO spawn pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads OBJECT_FIELD_SCALE_X (field 4, cross-expansion) from CREATE_OBJECT update fields and passes it through the full creature and game object spawn chain: game_handler callbacks → pending spawn structs → async load results → createInstance() calls. This gives boss giants, gnomes, children, and other non-unit-scale NPCs correct visual size, and ensures scaled GOs (e.g. large treasure chests, oversized plants) render at the server-specified scale rather than always at 1.0. - Added OBJECT_FIELD_SCALE_X to UF enum and all expansion update_fields.json - Added float scale to CreatureSpawnCallback and GameObjectSpawnCallback - Propagated scale through PendingCreatureSpawn, PreparedCreatureModel, PendingGameObjectSpawn, PreparedGameObjectWMO - Used scale in charRenderer/m2Renderer/wmoRenderer createInstance() calls - Sanity-clamped raw float to [0.01, 100.0] range before use --- Data/expansions/classic/update_fields.json | 1 + Data/expansions/tbc/update_fields.json | 1 + Data/expansions/turtle/update_fields.json | 1 + Data/expansions/wotlk/update_fields.json | 1 + include/core/application.hpp | 8 +++- include/game/game_handler.hpp | 8 ++-- include/game/update_field_table.hpp | 1 + src/core/application.cpp | 45 ++++++++++++++-------- src/game/game_handler.cpp | 39 +++++++++++++++++-- src/game/update_field_table.cpp | 4 ++ 10 files changed, 85 insertions(+), 24 deletions(-) diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json index 0d61eacc..c393c6e6 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -1,5 +1,6 @@ { "OBJECT_FIELD_ENTRY": 3, + "OBJECT_FIELD_SCALE_X": 4, "UNIT_FIELD_TARGET_LO": 16, "UNIT_FIELD_TARGET_HI": 17, "UNIT_FIELD_BYTES_0": 36, diff --git a/Data/expansions/tbc/update_fields.json b/Data/expansions/tbc/update_fields.json index c6d77c76..1df171f7 100644 --- a/Data/expansions/tbc/update_fields.json +++ b/Data/expansions/tbc/update_fields.json @@ -1,5 +1,6 @@ { "OBJECT_FIELD_ENTRY": 3, + "OBJECT_FIELD_SCALE_X": 4, "UNIT_FIELD_TARGET_LO": 16, "UNIT_FIELD_TARGET_HI": 17, "UNIT_FIELD_BYTES_0": 36, diff --git a/Data/expansions/turtle/update_fields.json b/Data/expansions/turtle/update_fields.json index a91a314b..b268c5f8 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -1,5 +1,6 @@ { "OBJECT_FIELD_ENTRY": 3, + "OBJECT_FIELD_SCALE_X": 4, "UNIT_FIELD_TARGET_LO": 16, "UNIT_FIELD_TARGET_HI": 17, "UNIT_FIELD_BYTES_0": 36, diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index 67019c80..0bff52fc 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -1,5 +1,6 @@ { "OBJECT_FIELD_ENTRY": 3, + "OBJECT_FIELD_SCALE_X": 4, "UNIT_FIELD_TARGET_LO": 6, "UNIT_FIELD_TARGET_HI": 7, "UNIT_FIELD_BYTES_0": 23, diff --git a/include/core/application.hpp b/include/core/application.hpp index 570f0658..7da1469b 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -98,7 +98,7 @@ private: void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); 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); + 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, @@ -113,7 +113,7 @@ private: 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); + 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; @@ -214,6 +214,7 @@ private: uint32_t displayId; uint32_t modelId; float x, y, z, orientation; + float scale = 1.0f; std::shared_ptr model; // parsed on background thread std::unordered_map predecodedTextures; // decoded on bg thread bool valid = false; @@ -300,6 +301,7 @@ private: uint64_t guid; uint32_t displayId; float x, y, z, orientation; + float scale = 1.0f; }; std::deque pendingCreatureSpawns_; static constexpr int MAX_SPAWNS_PER_FRAME = 3; @@ -393,6 +395,7 @@ private: uint32_t entry; uint32_t displayId; float x, y, z, orientation; + float scale = 1.0f; }; std::vector pendingGameObjectSpawns_; void processGameObjectSpawnQueue(); @@ -403,6 +406,7 @@ private: uint32_t entry; uint32_t displayId; float x, y, z, orientation; + float scale = 1.0f; std::shared_ptr wmoModel; std::unordered_map predecodedTextures; // decoded on bg thread bool valid = false; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index c0ec24c6..8656b9db 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -735,8 +735,8 @@ public: void setHearthstonePreloadCallback(HearthstonePreloadCallback cb) { hearthstonePreloadCallback_ = std::move(cb); } // Creature spawn callback (online mode - triggered when creature enters view) - // Parameters: guid, displayId, x, y, z (canonical), orientation - using CreatureSpawnCallback = std::function; + // Parameters: guid, displayId, x, y, z (canonical), orientation, scale (OBJECT_FIELD_SCALE_X) + using CreatureSpawnCallback = std::function; void setCreatureSpawnCallback(CreatureSpawnCallback cb) { creatureSpawnCallback_ = std::move(cb); } // Creature despawn callback (online mode - triggered when creature leaves view) @@ -766,8 +766,8 @@ public: void setPlayerEquipmentCallback(PlayerEquipmentCallback cb) { playerEquipmentCallback_ = std::move(cb); } // GameObject spawn callback (online mode - triggered when gameobject enters view) - // Parameters: guid, entry, displayId, x, y, z (canonical), orientation - using GameObjectSpawnCallback = std::function; + // Parameters: guid, entry, displayId, x, y, z (canonical), orientation, scale (OBJECT_FIELD_SCALE_X) + using GameObjectSpawnCallback = std::function; void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); } // GameObject move callback (online mode - triggered when gameobject position updates) diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 67651b00..88ce8a30 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -14,6 +14,7 @@ namespace game { enum class UF : uint16_t { // Object fields OBJECT_FIELD_ENTRY, + OBJECT_FIELD_SCALE_X, // Unit fields UNIT_FIELD_TARGET_LO, diff --git a/src/core/application.cpp b/src/core/application.cpp index 8a7b51e4..b04a5269 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -985,6 +985,18 @@ void Application::update(float deltaTime) { retrySpawn.y = unit->getY(); retrySpawn.z = unit->getZ(); retrySpawn.orientation = unit->getOrientation(); + { + using game::fieldIndex; using game::UF; + uint16_t si = fieldIndex(UF::OBJECT_FIELD_SCALE_X); + if (si != 0xFFFF) { + uint32_t raw = unit->getField(si); + if (raw != 0) { + float s2 = 1.0f; + std::memcpy(&s2, &raw, sizeof(float)); + if (s2 > 0.01f && s2 < 100.0f) retrySpawn.scale = s2; + } + } + } pendingCreatureSpawns_.push_back(retrySpawn); pendingCreatureSpawnGuids_.insert(guid); } @@ -2198,12 +2210,12 @@ void Application::setupUICallbacks() { // Faction hostility map is built in buildFactionHostilityMap() when character enters world // Creature spawn callback (online mode) - spawn creature models - gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { + gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale) { // Queue spawns to avoid hanging when many creatures appear at once. // Deduplicate so repeated updates don't flood pending queue. if (creatureInstances_.count(guid)) return; if (pendingCreatureSpawnGuids_.count(guid)) return; - pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation}); + pendingCreatureSpawns_.push_back({guid, displayId, x, y, z, orientation, scale}); pendingCreatureSpawnGuids_.insert(guid); }); @@ -2249,8 +2261,8 @@ void Application::setupUICallbacks() { }); // GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.) - gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { - pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation}); + gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale) { + pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation, scale}); }); // GameObject despawn callback (online mode) - remove static models @@ -4754,7 +4766,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Process ALL pending game object spawns. while (!pendingGameObjectSpawns_.empty()) { auto& s = pendingGameObjectSpawns_.front(); - spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation); + spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale); pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin()); } @@ -5285,7 +5297,7 @@ pipeline::M2Model Application::loadCreatureM2Sync(const std::string& m2Path) { return model; } -void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { +void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale) { if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return; // Skip if lookups not yet built (asset manager not ready) @@ -5722,9 +5734,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Convert canonical WoW orientation (0=north) -> render yaw (0=west) float renderYaw = orientation + glm::radians(90.0f); - // Create instance + // Create instance (apply server-provided scale from OBJECT_FIELD_SCALE_X) uint32_t instanceId = charRenderer->createInstance(modelId, renderPos, - glm::vec3(0.0f, 0.0f, renderYaw), 1.0f); + glm::vec3(0.0f, 0.0f, renderYaw), scale); if (instanceId == 0) { LOG_WARNING("Failed to create creature instance for guid 0x", std::hex, guid, std::dec); @@ -7035,7 +7047,7 @@ void Application::despawnOnlinePlayer(uint64_t guid) { creatureWasWalking_.erase(guid); } -void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { +void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale) { if (!renderer || !assetManager) return; if (!gameObjectLookupsBuilt_) { @@ -7192,7 +7204,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t if (loadedAsWmo) { uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos, - glm::vec3(0.0f, 0.0f, renderYawWmo), 1.0f); + glm::vec3(0.0f, 0.0f, renderYawWmo), scale); if (instanceId == 0) { LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec); return; @@ -7300,7 +7312,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t } uint32_t instanceId = m2Renderer->createInstance(modelId, renderPos, - glm::vec3(0.0f, 0.0f, renderYawM2go), 1.0f); + glm::vec3(0.0f, 0.0f, renderYawM2go), scale); if (instanceId == 0) { LOG_WARNING("Failed to create gameobject instance for guid 0x", std::hex, guid, std::dec); return; @@ -7418,6 +7430,7 @@ void Application::processAsyncCreatureResults(bool unlimited) { s.y = result.y; s.z = result.z; s.orientation = result.orientation; + s.scale = result.scale; pendingCreatureSpawns_.push_back(s); pendingCreatureSpawnGuids_.insert(result.guid); } @@ -7732,6 +7745,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) { result.y = s.y; result.z = s.z; result.orientation = s.orientation; + result.scale = s.scale; auto m2Data = am->readFile(m2Path); if (m2Data.empty()) { @@ -7810,7 +7824,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) { // Cached model — spawn is fast (no file I/O, just instance creation + texture setup) { auto spawnStart = std::chrono::steady_clock::now(); - spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation); + spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation, s.scale); auto spawnEnd = std::chrono::steady_clock::now(); float spawnMs = std::chrono::duration(spawnEnd - spawnStart).count(); if (spawnMs > 100.0f) { @@ -8015,7 +8029,7 @@ void Application::processAsyncGameObjectResults() { if (!result.valid || !result.isWmo || !result.wmoModel) { // Fallback: spawn via sync path (likely an M2 or failed WMO) spawnOnlineGameObject(result.guid, result.entry, result.displayId, - result.x, result.y, result.z, result.orientation); + result.x, result.y, result.z, result.orientation, result.scale); continue; } @@ -8042,7 +8056,7 @@ void Application::processAsyncGameObjectResults() { glm::vec3 renderPos = core::coords::canonicalToRender( glm::vec3(result.x, result.y, result.z)); uint32_t instanceId = wmoRenderer->createInstance( - modelId, renderPos, glm::vec3(0.0f, 0.0f, result.orientation), 1.0f); + modelId, renderPos, glm::vec3(0.0f, 0.0f, result.orientation), result.scale); if (instanceId == 0) continue; gameObjectInstances_[result.guid] = {modelId, instanceId, true}; @@ -8129,6 +8143,7 @@ void Application::processGameObjectSpawnQueue() { result.y = capture.y; result.z = capture.z; result.orientation = capture.orientation; + result.scale = capture.scale; result.modelPath = capturePath; result.isWmo = true; @@ -8194,7 +8209,7 @@ void Application::processGameObjectSpawnQueue() { } // Cached WMO or M2 — spawn synchronously (cheap) - spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation); + spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale); pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin()); } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 360e2312..9964828e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8009,8 +8009,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { LOG_DEBUG("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, " displayId=", unit->getDisplayId(), " at (", unit->getX(), ",", unit->getY(), ",", unit->getZ(), ")"); + float unitScale = 1.0f; + { + uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); + if (scaleIdx != 0xFFFF) { + uint32_t raw = entity->getField(scaleIdx); + if (raw != 0) { + std::memcpy(&unitScale, &raw, sizeof(float)); + if (unitScale <= 0.01f || unitScale > 100.0f) unitScale = 1.0f; + } + } + } creatureSpawnCallback_(block.guid, unit->getDisplayId(), - unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale); if (unitInitiallyDead && npcDeathCallback_) { npcDeathCallback_(block.guid); } @@ -8060,8 +8071,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Note: TransportSpawnCallback will be invoked from Application after WMO instance is created } if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) { + float goScale = 1.0f; + { + uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); + if (scaleIdx != 0xFFFF) { + uint32_t raw = entity->getField(scaleIdx); + if (raw != 0) { + std::memcpy(&goScale, &raw, sizeof(float)); + if (goScale <= 0.01f || goScale > 100.0f) goScale = 1.0f; + } + } + } gameObjectSpawnCallback_(block.guid, go->getEntry(), go->getDisplayId(), - go->getX(), go->getY(), go->getZ(), go->getOrientation()); + go->getX(), go->getY(), go->getZ(), go->getOrientation(), goScale); } // Fire transport move callback for transports (position update on re-creation) if (transportGuids_.count(block.guid) && transportMoveCallback_) { @@ -8366,8 +8388,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } } else if (creatureSpawnCallback_) { + float unitScale2 = 1.0f; + { + uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); + if (scaleIdx != 0xFFFF) { + uint32_t raw = entity->getField(scaleIdx); + if (raw != 0) { + std::memcpy(&unitScale2, &raw, sizeof(float)); + if (unitScale2 <= 0.01f || unitScale2 > 100.0f) unitScale2 = 1.0f; + } + } + } creatureSpawnCallback_(block.guid, unit->getDisplayId(), - unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale2); bool isDeadNow = (unit->getHealth() == 0) || ((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0); if (isDeadNow && !npcDeathNotified && npcDeathCallback_) { diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index 505b86e6..1026c29e 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -19,6 +19,7 @@ struct UFNameEntry { static const UFNameEntry kUFNames[] = { {"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY}, + {"OBJECT_FIELD_SCALE_X", UF::OBJECT_FIELD_SCALE_X}, {"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO}, {"UNIT_FIELD_TARGET_HI", UF::UNIT_FIELD_TARGET_HI}, {"UNIT_FIELD_BYTES_0", UF::UNIT_FIELD_BYTES_0}, @@ -52,6 +53,9 @@ static const UFNameEntry kUFNames[] = { {"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START}, {"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID}, {"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT}, + {"ITEM_FIELD_DURABILITY", UF::ITEM_FIELD_DURABILITY}, + {"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY}, + {"PLAYER_REST_STATE_EXPERIENCE", UF::PLAYER_REST_STATE_EXPERIENCE}, {"CONTAINER_FIELD_NUM_SLOTS", UF::CONTAINER_FIELD_NUM_SLOTS}, {"CONTAINER_FIELD_SLOT_1", UF::CONTAINER_FIELD_SLOT_1}, };