Kelsidavis-WoWee/src/game/entity_controller.cpp

2138 lines
99 KiB
C++
Raw Normal View History

refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
#include "game/entity_controller.hpp"
#include "game/game_handler.hpp"
#include "game/game_utils.hpp"
#include "game/packet_parsers.hpp"
#include "game/entity.hpp"
#include "game/update_field_table.hpp"
#include "game/opcode_table.hpp"
#include "game/chat_handler.hpp"
#include "game/transport_manager.hpp"
#include "core/logger.hpp"
#include "core/coordinates.hpp"
#include "network/world_socket.hpp"
#include <algorithm>
#include <cstring>
#include <zlib.h>
namespace wowee {
namespace game {
namespace {
const char* worldStateName(WorldState state) {
switch (state) {
case WorldState::DISCONNECTED: return "DISCONNECTED";
case WorldState::CONNECTING: return "CONNECTING";
case WorldState::CONNECTED: return "CONNECTED";
case WorldState::CHALLENGE_RECEIVED: return "CHALLENGE_RECEIVED";
case WorldState::AUTH_SENT: return "AUTH_SENT";
case WorldState::AUTHENTICATED: return "AUTHENTICATED";
case WorldState::READY: return "READY";
case WorldState::CHAR_LIST_REQUESTED: return "CHAR_LIST_REQUESTED";
case WorldState::CHAR_LIST_RECEIVED: return "CHAR_LIST_RECEIVED";
case WorldState::ENTERING_WORLD: return "ENTERING_WORLD";
case WorldState::IN_WORLD: return "IN_WORLD";
case WorldState::FAILED: return "FAILED";
}
return "UNKNOWN";
}
bool envFlagEnabled(const char* key, bool defaultValue = false) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
raw[0] == 'n' || raw[0] == 'N');
}
int parseEnvIntClamped(const char* key, int defaultValue, int minValue, int maxValue) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
char* end = nullptr;
long parsed = std::strtol(raw, &end, 10);
if (end == raw) return defaultValue;
return static_cast<int>(std::clamp<long>(parsed, minValue, maxValue));
}
int updateObjectBlocksBudgetPerUpdate(WorldState state) {
static const int inWorldBudget =
parseEnvIntClamped("WOWEE_NET_MAX_UPDATE_OBJECT_BLOCKS", 24, 1, 2048);
static const int loginBudget =
parseEnvIntClamped("WOWEE_NET_MAX_UPDATE_OBJECT_BLOCKS_LOGIN", 128, 1, 4096);
return state == WorldState::IN_WORLD ? inWorldBudget : loginBudget;
}
float slowUpdateObjectBlockLogThresholdMs() {
static const int thresholdMs =
parseEnvIntClamped("WOWEE_NET_SLOW_UPDATE_BLOCK_LOG_MS", 10, 1, 60000);
return static_cast<float>(thresholdMs);
}
} // anonymous namespace
EntityController::EntityController(GameHandler& owner)
: owner_(owner) { initTypeHandlers(); }
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
void EntityController::registerOpcodes(DispatchTable& table) {
// World object updates
table[Opcode::SMSG_UPDATE_OBJECT] = [this](network::Packet& packet) {
LOG_DEBUG("Received SMSG_UPDATE_OBJECT, state=", static_cast<int>(owner_.state), " size=", packet.getSize());
if (owner_.state == WorldState::IN_WORLD) handleUpdateObject(packet);
};
table[Opcode::SMSG_COMPRESSED_UPDATE_OBJECT] = [this](network::Packet& packet) {
LOG_DEBUG("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast<int>(owner_.state), " size=", packet.getSize());
if (owner_.state == WorldState::IN_WORLD) handleCompressedUpdateObject(packet);
};
table[Opcode::SMSG_DESTROY_OBJECT] = [this](network::Packet& packet) {
if (owner_.state == WorldState::IN_WORLD) handleDestroyObject(packet);
};
// Entity queries
table[Opcode::SMSG_NAME_QUERY_RESPONSE] = [this](network::Packet& packet) {
handleNameQueryResponse(packet);
};
table[Opcode::SMSG_CREATURE_QUERY_RESPONSE] = [this](network::Packet& packet) {
handleCreatureQueryResponse(packet);
};
table[Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE] = [this](network::Packet& packet) {
handleGameObjectQueryResponse(packet);
};
table[Opcode::SMSG_GAMEOBJECT_PAGETEXT] = [this](network::Packet& packet) {
handleGameObjectPageText(packet);
};
table[Opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE] = [this](network::Packet& packet) {
handlePageTextQueryResponse(packet);
};
}
void EntityController::clearAll() {
pendingUpdateObjectWork_.clear();
playerNameCache.clear();
playerClassRaceCache_.clear();
pendingNameQueries.clear();
creatureInfoCache.clear();
pendingCreatureQueries.clear();
gameObjectInfoCache_.clear();
pendingGameObjectQueries_.clear();
transportGuids_.clear();
serverUpdatedTransportGuids_.clear();
entityManager.clear();
}
// ============================================================
// Update Object Pipeline
// ============================================================
void EntityController::enqueueUpdateObjectWork(UpdateObjectData&& data) {
pendingUpdateObjectWork_.push_back(PendingUpdateObjectWork{std::move(data)});
}
void EntityController::processPendingUpdateObjectWork(const std::chrono::steady_clock::time_point& start,
float budgetMs) {
if (pendingUpdateObjectWork_.empty()) {
return;
}
const int maxBlocksThisUpdate = updateObjectBlocksBudgetPerUpdate(owner_.state);
int processedBlocks = 0;
while (!pendingUpdateObjectWork_.empty() && processedBlocks < maxBlocksThisUpdate) {
float elapsedMs = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - start).count();
if (elapsedMs >= budgetMs) {
break;
}
auto& work = pendingUpdateObjectWork_.front();
if (!work.outOfRangeProcessed) {
auto outOfRangeStart = std::chrono::steady_clock::now();
processOutOfRangeObjects(work.data.outOfRangeGuids);
float outOfRangeMs = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - outOfRangeStart).count();
if (outOfRangeMs > slowUpdateObjectBlockLogThresholdMs()) {
LOG_WARNING("SLOW update-object out-of-range handling: ", outOfRangeMs,
"ms guidCount=", work.data.outOfRangeGuids.size());
}
work.outOfRangeProcessed = true;
}
while (work.nextBlockIndex < work.data.blocks.size() && processedBlocks < maxBlocksThisUpdate) {
elapsedMs = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - start).count();
if (elapsedMs >= budgetMs) {
break;
}
const UpdateBlock& block = work.data.blocks[work.nextBlockIndex];
auto blockStart = std::chrono::steady_clock::now();
applyUpdateObjectBlock(block, work.newItemCreated);
float blockMs = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - blockStart).count();
if (blockMs > slowUpdateObjectBlockLogThresholdMs()) {
LOG_WARNING("SLOW update-object block apply: ", blockMs,
"ms index=", work.nextBlockIndex,
" type=", static_cast<int>(block.updateType),
" guid=0x", std::hex, block.guid, std::dec,
" objectType=", static_cast<int>(block.objectType),
" fieldCount=", block.fields.size(),
" hasMovement=", block.hasMovement ? 1 : 0);
}
++work.nextBlockIndex;
++processedBlocks;
}
if (work.nextBlockIndex >= work.data.blocks.size()) {
finalizeUpdateObjectBatch(work.newItemCreated);
pendingUpdateObjectWork_.pop_front();
continue;
}
break;
}
if (!pendingUpdateObjectWork_.empty()) {
const auto& work = pendingUpdateObjectWork_.front();
LOG_DEBUG("GameHandler update-object budget reached (remainingBatches=",
pendingUpdateObjectWork_.size(), ", nextBlockIndex=", work.nextBlockIndex,
"/", work.data.blocks.size(), ", owner_.state=", worldStateName(owner_.state), ")");
}
}
void EntityController::handleUpdateObject(network::Packet& packet) {
UpdateObjectData data;
if (!owner_.packetParsers_->parseUpdateObject(packet, data)) {
static int updateObjErrors = 0;
if (++updateObjErrors <= 5)
LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT");
if (data.blocks.empty()) return;
// Fall through: process any blocks that were successfully parsed before the failure.
}
enqueueUpdateObjectWork(std::move(data));
}
void EntityController::processOutOfRangeObjects(const std::vector<uint64_t>& guids) {
// Process out-of-range objects first
for (uint64_t guid : guids) {
auto entity = entityManager.getEntity(guid);
if (!entity) continue;
const bool isKnownTransport = transportGuids_.count(guid) > 0;
if (isKnownTransport) {
// Keep transports alive across out-of-range flapping.
// Boats/zeppelins are global movers and removing them here can make
// them disappear until a later movement snapshot happens to recreate them.
const bool playerAboardNow = (owner_.playerTransportGuid_ == guid);
const bool stickyAboard = (owner_.playerTransportStickyGuid_ == guid && owner_.playerTransportStickyTimer_ > 0.0f);
const bool movementSaysAboard = (owner_.movementInfo.transportGuid == guid);
LOG_INFO("Preserving transport on out-of-range: 0x",
std::hex, guid, std::dec,
" now=", playerAboardNow,
" sticky=", stickyAboard,
" movement=", movementSaysAboard);
continue;
}
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
// Trigger despawn callbacks before removing entity
if (entity->getType() == ObjectType::UNIT && owner_.creatureDespawnCallback_) {
owner_.creatureDespawnCallback_(guid);
} else if (entity->getType() == ObjectType::PLAYER && owner_.playerDespawnCallback_) {
owner_.playerDespawnCallback_(guid);
owner_.otherPlayerVisibleItemEntries_.erase(guid);
owner_.otherPlayerVisibleDirty_.erase(guid);
owner_.otherPlayerMoveTimeMs_.erase(guid);
owner_.inspectedPlayerItemEntries_.erase(guid);
owner_.pendingAutoInspect_.erase(guid);
// Clear pending name query so the query is re-sent when this player
// comes back into range (entity is recreated as a new object).
pendingNameQueries.erase(guid);
} else if (entity->getType() == ObjectType::GAMEOBJECT && owner_.gameObjectDespawnCallback_) {
owner_.gameObjectDespawnCallback_(guid);
}
transportGuids_.erase(guid);
serverUpdatedTransportGuids_.erase(guid);
owner_.clearTransportAttachment(guid);
if (owner_.playerTransportGuid_ == guid) {
owner_.clearPlayerTransport();
}
entityManager.removeEntity(guid);
}
}
// ============================================================
// Phase 1: Extracted helper methods
// ============================================================
bool EntityController::extractPlayerAppearance(const std::map<uint16_t, uint32_t>& fields,
uint8_t& outRace,
uint8_t& outGender,
uint32_t& outAppearanceBytes,
uint8_t& outFacial) const {
outRace = 0;
outGender = 0;
outAppearanceBytes = 0;
outFacial = 0;
auto readField = [&](uint16_t idx, uint32_t& out) -> bool {
if (idx == 0xFFFF) return false;
auto it = fields.find(idx);
if (it == fields.end()) return false;
out = it->second;
return true;
};
uint32_t bytes0 = 0;
uint32_t pbytes = 0;
uint32_t pbytes2 = 0;
const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0);
const uint16_t ufPbytes = fieldIndex(UF::PLAYER_BYTES);
const uint16_t ufPbytes2 = fieldIndex(UF::PLAYER_BYTES_2);
bool haveBytes0 = readField(ufBytes0, bytes0);
bool havePbytes = readField(ufPbytes, pbytes);
bool havePbytes2 = readField(ufPbytes2, pbytes2);
// Heuristic fallback: Turtle can run with unusual build numbers; if the JSON table is missing,
// try to locate plausible packed fields by scanning.
if (!haveBytes0) {
for (const auto& [idx, v] : fields) {
uint8_t race = static_cast<uint8_t>(v & 0xFF);
uint8_t cls = static_cast<uint8_t>((v >> 8) & 0xFF);
uint8_t gender = static_cast<uint8_t>((v >> 16) & 0xFF);
uint8_t power = static_cast<uint8_t>((v >> 24) & 0xFF);
if (race >= 1 && race <= 20 &&
cls >= 1 && cls <= 20 &&
gender <= 1 &&
power <= 10) {
bytes0 = v;
haveBytes0 = true;
break;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
if (!havePbytes) {
for (const auto& [idx, v] : fields) {
uint8_t skin = static_cast<uint8_t>(v & 0xFF);
uint8_t face = static_cast<uint8_t>((v >> 8) & 0xFF);
uint8_t hair = static_cast<uint8_t>((v >> 16) & 0xFF);
uint8_t color = static_cast<uint8_t>((v >> 24) & 0xFF);
if (skin <= 50 && face <= 50 && hair <= 100 && color <= 50) {
pbytes = v;
havePbytes = true;
break;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
if (!havePbytes2) {
for (const auto& [idx, v] : fields) {
uint8_t facial = static_cast<uint8_t>(v & 0xFF);
if (facial <= 100) {
pbytes2 = v;
havePbytes2 = true;
break;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (!haveBytes0 || !havePbytes) return false;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
outRace = static_cast<uint8_t>(bytes0 & 0xFF);
outGender = static_cast<uint8_t>((bytes0 >> 16) & 0xFF);
outAppearanceBytes = pbytes;
outFacial = havePbytes2 ? static_cast<uint8_t>(pbytes2 & 0xFF) : 0;
return true;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
void EntityController::maybeDetectCoinageIndex(const std::map<uint16_t, uint32_t>& oldFields,
const std::map<uint16_t, uint32_t>& newFields) {
if (owner_.pendingMoneyDelta_ == 0 || owner_.pendingMoneyDeltaTimer_ <= 0.0f) return;
if (oldFields.empty() || newFields.empty()) return;
constexpr uint32_t kMaxPlausibleCoinage = 2147483647u;
std::vector<uint16_t> candidates;
candidates.reserve(8);
for (const auto& [idx, newVal] : newFields) {
auto itOld = oldFields.find(idx);
if (itOld == oldFields.end()) continue;
uint32_t oldVal = itOld->second;
if (newVal < oldVal) continue;
uint32_t delta = newVal - oldVal;
if (delta != owner_.pendingMoneyDelta_) continue;
if (newVal > kMaxPlausibleCoinage) continue;
candidates.push_back(idx);
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (candidates.empty()) return;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
uint16_t current = fieldIndex(UF::PLAYER_FIELD_COINAGE);
uint16_t chosen = candidates[0];
if (std::find(candidates.begin(), candidates.end(), current) != candidates.end()) {
chosen = current;
} else {
std::sort(candidates.begin(), candidates.end());
chosen = candidates[0];
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (chosen != current && current != 0xFFFF) {
owner_.updateFieldTable_.setIndex(UF::PLAYER_FIELD_COINAGE, chosen);
LOG_WARNING("Auto-detected PLAYER_FIELD_COINAGE index: ", chosen, " (was ", current, ")");
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
owner_.pendingMoneyDelta_ = 0;
owner_.pendingMoneyDeltaTimer_ = 0.0f;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// ============================================================
// Phase 2: Update type dispatch
// ============================================================
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
void EntityController::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
switch (block.updateType) {
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2:
handleCreateObject(block, newItemCreated);
break;
case UpdateType::VALUES:
handleValuesUpdate(block, newItemCreated);
break;
case UpdateType::MOVEMENT:
handleMovementUpdate(block);
break;
default:
break;
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// ============================================================
// Phase 3: Concern-specific helpers
// ============================================================
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3i: Non-player transport child attachment — identical in CREATE/VALUES/MOVEMENT
void EntityController::updateNonPlayerTransportAttachment(const UpdateBlock& block,
const std::shared_ptr<Entity>& entity,
ObjectType entityType) {
if (block.guid == owner_.playerGuid) return;
if (entityType != ObjectType::UNIT && entityType != ObjectType::GAMEOBJECT) return;
if (block.onTransport && block.transportGuid != 0) {
glm::vec3 localOffset = core::coords::serverToCanonical(
glm::vec3(block.transportX, block.transportY, block.transportZ));
const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING
float localOriCanonical = core::coords::normalizeAngleRad(-block.transportO);
owner_.setTransportAttachment(block.guid, entityType, block.transportGuid,
localOffset, hasLocalOrientation, localOriCanonical);
if (owner_.transportManager_ && owner_.transportManager_->getTransport(block.transportGuid)) {
glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
}
} else {
owner_.clearTransportAttachment(block.guid);
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3f: Rebuild playerAuras from UNIT_FIELD_AURAS (Classic/vanilla only).
// blockFields is used to check if any aura field was updated in this packet.
// entity->getFields() is used for reading the full accumulated state.
// Note: CREATE originally normalised Classic flags (0x02→0x80) while VALUES
// used raw bytes; VALUES runs more frequently and overwrites CREATE's mapping
// immediately, so the helper uses raw bytes (matching VALUES behaviour).
void EntityController::syncClassicAurasFromFields(const std::shared_ptr<Entity>& entity) {
if (!isClassicLikeExpansion() || !owner_.spellHandler_) return;
const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS);
const uint16_t ufAuraFlags = fieldIndex(UF::UNIT_FIELD_AURAFLAGS);
if (ufAuras == 0xFFFF) return;
const auto& allFields = entity->getFields();
bool hasAuraField = false;
for (const auto& [fk, fv] : allFields) {
if (fk >= ufAuras && fk < ufAuras + 48) { hasAuraField = true; break; }
}
if (!hasAuraField) return;
owner_.spellHandler_->playerAuras_.clear();
owner_.spellHandler_->playerAuras_.resize(48);
uint64_t nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
for (int slot = 0; slot < 48; ++slot) {
auto it = allFields.find(static_cast<uint16_t>(ufAuras + slot));
if (it != allFields.end() && it->second != 0) {
AuraSlot& a = owner_.spellHandler_->playerAuras_[slot];
a.spellId = it->second;
// Read aura flag byte: packed 4-per-uint32 at ufAuraFlags
uint8_t aFlag = 0;
if (ufAuraFlags != 0xFFFF) {
auto fit = allFields.find(static_cast<uint16_t>(ufAuraFlags + slot / 4));
if (fit != allFields.end())
aFlag = static_cast<uint8_t>((fit->second >> ((slot % 4) * 8)) & 0xFF);
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
a.flags = aFlag;
a.durationMs = -1;
a.maxDurationMs = -1;
a.casterGuid = owner_.playerGuid;
a.receivedAtMs = nowMs;
}
}
LOG_DEBUG("[Classic] Rebuilt playerAuras from UNIT_FIELD_AURAS");
pendingEvents_.emit("UNIT_AURA", {"player"});
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3h: Detect player mount/dismount from UNIT_FIELD_MOUNTDISPLAYID changes
void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
const std::map<uint16_t, uint32_t>& blockFields) {
uint32_t old = owner_.currentMountDisplayId_;
owner_.currentMountDisplayId_ = newMountDisplayId;
if (newMountDisplayId != old && owner_.mountCallback_) owner_.mountCallback_(newMountDisplayId);
if (newMountDisplayId != old)
pendingEvents_.emit("UNIT_MODEL_CHANGED", {"player"});
if (old == 0 && newMountDisplayId != 0) {
// Just mounted — find the mount aura (indefinite duration, self-cast)
owner_.mountAuraSpellId_ = 0;
if (owner_.spellHandler_) for (const auto& a : owner_.spellHandler_->playerAuras_) {
if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == owner_.playerGuid) {
owner_.mountAuraSpellId_ = a.spellId;
}
}
// Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block
if (owner_.mountAuraSpellId_ == 0) {
const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS);
if (ufAuras != 0xFFFF) {
for (const auto& [fk, fv] : blockFields) {
if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) {
owner_.mountAuraSpellId_ = fv;
break;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
}
LOG_INFO("Mount detected: displayId=", newMountDisplayId, " auraSpellId=", owner_.mountAuraSpellId_);
}
if (old != 0 && newMountDisplayId == 0) {
owner_.mountAuraSpellId_ = 0;
if (owner_.spellHandler_) for (auto& a : owner_.spellHandler_->playerAuras_)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// Phase 4: Resolve cached field indices once per handler call.
EntityController::UnitFieldIndices EntityController::UnitFieldIndices::resolve() {
return UnitFieldIndices{
fieldIndex(UF::UNIT_FIELD_HEALTH),
fieldIndex(UF::UNIT_FIELD_MAXHEALTH),
fieldIndex(UF::UNIT_FIELD_POWER1),
fieldIndex(UF::UNIT_FIELD_MAXPOWER1),
fieldIndex(UF::UNIT_FIELD_LEVEL),
fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE),
fieldIndex(UF::UNIT_FIELD_FLAGS),
fieldIndex(UF::UNIT_DYNAMIC_FLAGS),
fieldIndex(UF::UNIT_FIELD_DISPLAYID),
fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID),
fieldIndex(UF::UNIT_NPC_FLAGS),
fieldIndex(UF::UNIT_FIELD_BYTES_0),
fieldIndex(UF::UNIT_FIELD_BYTES_1)
};
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
EntityController::PlayerFieldIndices EntityController::PlayerFieldIndices::resolve() {
return PlayerFieldIndices{
fieldIndex(UF::PLAYER_XP),
fieldIndex(UF::PLAYER_NEXT_LEVEL_XP),
fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE),
fieldIndex(UF::UNIT_FIELD_LEVEL),
fieldIndex(UF::PLAYER_FIELD_COINAGE),
fieldIndex(UF::PLAYER_FIELD_HONOR_CURRENCY),
fieldIndex(UF::PLAYER_FIELD_ARENA_CURRENCY),
fieldIndex(UF::PLAYER_FLAGS),
fieldIndex(UF::UNIT_FIELD_RESISTANCES),
fieldIndex(UF::PLAYER_BYTES),
fieldIndex(UF::PLAYER_BYTES_2),
fieldIndex(UF::PLAYER_CHOSEN_TITLE),
{fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1),
fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3),
fieldIndex(UF::UNIT_FIELD_STAT4)},
fieldIndex(UF::UNIT_FIELD_ATTACK_POWER),
fieldIndex(UF::UNIT_FIELD_RANGED_ATTACK_POWER),
fieldIndex(UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS),
fieldIndex(UF::PLAYER_FIELD_MOD_HEALING_DONE_POS),
fieldIndex(UF::PLAYER_BLOCK_PERCENTAGE),
fieldIndex(UF::PLAYER_DODGE_PERCENTAGE),
fieldIndex(UF::PLAYER_PARRY_PERCENTAGE),
fieldIndex(UF::PLAYER_CRIT_PERCENTAGE),
fieldIndex(UF::PLAYER_RANGED_CRIT_PERCENTAGE),
fieldIndex(UF::PLAYER_SPELL_CRIT_PERCENTAGE1),
fieldIndex(UF::PLAYER_FIELD_COMBAT_RATING_1)
};
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3a: Create the appropriate Entity subclass from the block's object type.
std::shared_ptr<Entity> EntityController::createEntityFromBlock(const UpdateBlock& block) {
switch (block.objectType) {
case ObjectType::PLAYER:
return std::make_shared<Player>(block.guid);
case ObjectType::UNIT:
return std::make_shared<Unit>(block.guid);
case ObjectType::GAMEOBJECT:
return std::make_shared<GameObject>(block.guid);
default: {
auto entity = std::make_shared<Entity>(block.guid);
entity->setType(block.objectType);
return entity;
}
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3b: Track player-on-transport state from movement blocks.
// Consolidates near-identical logic from CREATE and MOVEMENT handlers.
// When updateMovementInfoPos is true (MOVEMENT), movementInfo.x/y/z are set
// to the raw canonical position when not on a resolved transport.
// When false (CREATE), movementInfo is only set for resolved transport positions.
void EntityController::applyPlayerTransportState(const UpdateBlock& block,
const std::shared_ptr<Entity>& entity,
const glm::vec3& canonicalPos, float oCanonical,
bool updateMovementInfoPos) {
if (block.onTransport) {
// Convert transport offset from server → canonical coordinates
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset);
owner_.setPlayerOnTransport(block.transportGuid, canonicalOffset);
if (owner_.transportManager_ && owner_.transportManager_->getTransport(owner_.playerTransportGuid_)) {
glm::vec3 composed = owner_.transportManager_->getPlayerWorldPosition(owner_.playerTransportGuid_, owner_.playerTransportOffset_);
entity->setPosition(composed.x, composed.y, composed.z, oCanonical);
owner_.movementInfo.x = composed.x;
owner_.movementInfo.y = composed.y;
owner_.movementInfo.z = composed.z;
} else if (updateMovementInfoPos) {
owner_.movementInfo.x = canonicalPos.x;
owner_.movementInfo.y = canonicalPos.y;
owner_.movementInfo.z = canonicalPos.z;
}
LOG_INFO("Player on transport: 0x", std::hex, owner_.playerTransportGuid_, std::dec,
" offset=(", owner_.playerTransportOffset_.x, ", ", owner_.playerTransportOffset_.y,
", ", owner_.playerTransportOffset_.z, ")");
} else {
if (updateMovementInfoPos) {
owner_.movementInfo.x = canonicalPos.x;
owner_.movementInfo.y = canonicalPos.y;
owner_.movementInfo.z = canonicalPos.z;
}
// Don't clear client-side M2 transport boarding (trams) —
// the server doesn't know about client-detected transport attachment.
bool isClientM2Transport = false;
if (owner_.playerTransportGuid_ != 0 && owner_.transportManager_) {
auto* tr = owner_.transportManager_->getTransport(owner_.playerTransportGuid_);
isClientM2Transport = (tr && tr->isM2);
}
if (owner_.playerTransportGuid_ != 0 && !isClientM2Transport) {
LOG_INFO("Player left transport");
owner_.clearPlayerTransport();
}
}
}
// 3c: Apply unit fields during CREATE — sets health/power/level/flags/displayId/etc.
// Returns true if the entity is initially dead (health=0 or DYNFLAG_DEAD).
bool EntityController::applyUnitFieldsOnCreate(const UpdateBlock& block,
std::shared_ptr<Unit>& unit,
const UnitFieldIndices& ufi) {
bool unitInitiallyDead = false;
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001;
for (const auto& [key, val] : block.fields) {
// Check all specific fields BEFORE power/maxpower range checks.
// In Classic, power indices (23-27) are adjacent to maxHealth (28),
// and maxPower indices (29-33) are adjacent to level (34) and faction (35).
// A range check like "key >= powerBase && key < powerBase+7" would
// incorrectly capture maxHealth/level/faction in Classic's tight layout.
if (key == ufi.health) {
unit->setHealth(val);
if (block.objectType == ObjectType::UNIT && val == 0) {
unitInitiallyDead = true;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
if (block.guid == owner_.playerGuid && val == 0) {
owner_.playerDead_ = true;
LOG_INFO("Player logged in dead");
}
} else if (key == ufi.maxHealth) { unit->setMaxHealth(val); }
else if (key == ufi.level) {
unit->setLevel(val);
} else if (key == ufi.faction) {
unit->setFactionTemplate(val);
if (owner_.addonEventCallback_) {
auto uid = owner_.guidToUnitId(block.guid);
if (!uid.empty())
pendingEvents_.emit("UNIT_FACTION", {uid});
}
}
else if (key == ufi.flags) {
unit->setUnitFlags(val);
if (owner_.addonEventCallback_) {
auto uid = owner_.guidToUnitId(block.guid);
if (!uid.empty())
pendingEvents_.emit("UNIT_FLAGS", {uid});
}
}
else if (key == ufi.bytes0) {
unit->setPowerType(static_cast<uint8_t>((val >> 24) & 0xFF));
} else if (key == ufi.displayId) {
unit->setDisplayId(val);
if (owner_.addonEventCallback_) {
auto uid = owner_.guidToUnitId(block.guid);
if (!uid.empty())
pendingEvents_.emit("UNIT_MODEL_CHANGED", {uid});
}
}
else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
else if (key == ufi.dynFlags) {
unit->setDynamicFlags(val);
if (block.objectType == ObjectType::UNIT &&
((val & UNIT_DYNFLAG_DEAD) != 0 || (val & UNIT_DYNFLAG_LOOTABLE) != 0)) {
unitInitiallyDead = true;
}
}
// Power/maxpower range checks AFTER all specific fields
else if (key >= ufi.powerBase && key < ufi.powerBase + 7) {
unit->setPowerByType(static_cast<uint8_t>(key - ufi.powerBase), val);
} else if (key >= ufi.maxPowerBase && key < ufi.maxPowerBase + 7) {
unit->setMaxPowerByType(static_cast<uint8_t>(key - ufi.maxPowerBase), val);
}
else if (key == ufi.mountDisplayId) {
if (block.guid == owner_.playerGuid) {
detectPlayerMountChange(val, block.fields);
}
unit->setMountDisplayId(val);
}
}
return unitInitiallyDead;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3c: Apply unit fields during VALUES update — tracks health/power/display changes
// and fires events for transitions (death, resurrect, level up, etc.).
EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdate(
const UpdateBlock& block, const std::shared_ptr<Entity>& entity,
std::shared_ptr<Unit>& unit, const UnitFieldIndices& ufi) {
UnitFieldUpdateResult result;
result.oldDisplayId = unit->getDisplayId();
uint32_t oldHealth = unit->getHealth();
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
for (const auto& [key, val] : block.fields) {
if (key == ufi.health) {
unit->setHealth(val);
result.healthChanged = true;
if (val == 0) {
if (owner_.combatHandler_ && block.guid == owner_.combatHandler_->getAutoAttackTargetGuid()) {
owner_.stopAutoAttack();
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
if (owner_.combatHandler_) owner_.combatHandler_->removeHostileAttacker(block.guid);
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (block.guid == owner_.playerGuid) {
owner_.playerDead_ = true;
owner_.releasedSpirit_ = false;
owner_.stopAutoAttack();
// Cache death position as corpse location.
// Classic WoW does not send SMSG_DEATH_RELEASE_LOC, so
// this is the primary source for canReclaimCorpse().
// owner_.movementInfo is canonical (x=north, y=west); owner_.corpseX_/Y_
// are raw server coords (x=west, y=north) — swap axes.
owner_.corpseX_ = owner_.movementInfo.y; // canonical west = server X
owner_.corpseY_ = owner_.movementInfo.x; // canonical north = server Y
owner_.corpseZ_ = owner_.movementInfo.z;
owner_.corpseMapId_ = owner_.currentMapId_;
LOG_INFO("Player died! Corpse position cached at server=(",
owner_.corpseX_, ",", owner_.corpseY_, ",", owner_.corpseZ_,
") map=", owner_.corpseMapId_);
pendingEvents_.emit("PLAYER_DEAD", {});
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && owner_.npcDeathCallback_) {
owner_.npcDeathCallback_(block.guid);
result.npcDeathNotified = true;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
} else if (oldHealth == 0 && val > 0) {
if (block.guid == owner_.playerGuid) {
bool wasGhost = owner_.releasedSpirit_;
owner_.playerDead_ = false;
if (!wasGhost) {
LOG_INFO("Player resurrected!");
pendingEvents_.emit("PLAYER_ALIVE", {});
} else {
LOG_INFO("Player entered ghost form");
owner_.releasedSpirit_ = false;
pendingEvents_.emit("PLAYER_UNGHOST", {});
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && owner_.npcRespawnCallback_) {
owner_.npcRespawnCallback_(block.guid);
result.npcRespawnNotified = true;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
// Specific fields checked BEFORE power/maxpower range checks
// (Classic packs maxHealth/level/faction adjacent to power indices)
} else if (key == ufi.maxHealth) { unit->setMaxHealth(val); result.healthChanged = true; }
else if (key == ufi.bytes0) {
uint8_t oldPT = unit->getPowerType();
unit->setPowerType(static_cast<uint8_t>((val >> 24) & 0xFF));
if (unit->getPowerType() != oldPT) {
auto uid = owner_.guidToUnitId(block.guid);
if (!uid.empty())
pendingEvents_.emit("UNIT_DISPLAYPOWER", {uid});
}
} else if (key == ufi.flags) { unit->setUnitFlags(val); }
else if (ufi.bytes1 != 0xFFFF && key == ufi.bytes1 && block.guid == owner_.playerGuid) {
uint8_t newForm = static_cast<uint8_t>((val >> 24) & 0xFF);
if (newForm != owner_.shapeshiftFormId_) {
owner_.shapeshiftFormId_ = newForm;
LOG_INFO("Shapeshift form changed: ", static_cast<int>(newForm));
pendingEvents_.emit("UPDATE_SHAPESHIFT_FORM", {});
pendingEvents_.emit("UPDATE_SHAPESHIFT_FORMS", {});
}
}
else if (key == ufi.dynFlags) {
uint32_t oldDyn = unit->getDynamicFlags();
unit->setDynamicFlags(val);
if (block.guid == owner_.playerGuid) {
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
if (!wasDead && nowDead) {
owner_.playerDead_ = true;
owner_.releasedSpirit_ = false;
owner_.corpseX_ = owner_.movementInfo.y;
owner_.corpseY_ = owner_.movementInfo.x;
owner_.corpseZ_ = owner_.movementInfo.z;
owner_.corpseMapId_ = owner_.currentMapId_;
LOG_INFO("Player died (dynamic flags). Corpse cached map=", owner_.corpseMapId_);
} else if (wasDead && !nowDead) {
owner_.playerDead_ = false;
owner_.releasedSpirit_ = false;
owner_.selfResAvailable_ = false;
LOG_INFO("Player resurrected (dynamic flags)");
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
} else if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
if (!wasDead && nowDead) {
if (!result.npcDeathNotified && owner_.npcDeathCallback_) {
owner_.npcDeathCallback_(block.guid);
result.npcDeathNotified = true;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
} else if (wasDead && !nowDead) {
if (!result.npcRespawnNotified && owner_.npcRespawnCallback_) {
owner_.npcRespawnCallback_(block.guid);
result.npcRespawnNotified = true;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
} else if (key == ufi.level) {
uint32_t oldLvl = unit->getLevel();
unit->setLevel(val);
if (val != oldLvl) {
auto uid = owner_.guidToUnitId(block.guid);
if (!uid.empty())
pendingEvents_.emit("UNIT_LEVEL", {uid});
}
if (block.guid != owner_.playerGuid &&
entity->getType() == ObjectType::PLAYER &&
val > oldLvl && oldLvl > 0 &&
owner_.otherPlayerLevelUpCallback_) {
owner_.otherPlayerLevelUpCallback_(block.guid, val);
}
}
else if (key == ufi.faction) {
unit->setFactionTemplate(val);
unit->setHostile(owner_.isHostileFaction(val));
} else if (key == ufi.displayId) {
if (val != unit->getDisplayId()) {
unit->setDisplayId(val);
result.displayIdChanged = true;
}
} else if (key == ufi.mountDisplayId) {
if (block.guid == owner_.playerGuid) {
detectPlayerMountChange(val, block.fields);
}
unit->setMountDisplayId(val);
} else if (key == ufi.npcFlags) { unit->setNpcFlags(val); }
// Power/maxpower range checks AFTER all specific fields
else if (key >= ufi.powerBase && key < ufi.powerBase + 7) {
unit->setPowerByType(static_cast<uint8_t>(key - ufi.powerBase), val);
result.powerChanged = true;
} else if (key >= ufi.maxPowerBase && key < ufi.maxPowerBase + 7) {
unit->setMaxPowerByType(static_cast<uint8_t>(key - ufi.maxPowerBase), val);
result.powerChanged = true;
}
}
// Fire UNIT_HEALTH / UNIT_POWER events for Lua addons
if ((result.healthChanged || result.powerChanged)) {
auto unitId = owner_.guidToUnitId(block.guid);
if (!unitId.empty()) {
if (result.healthChanged) pendingEvents_.emit("UNIT_HEALTH", {unitId});
if (result.powerChanged) {
pendingEvents_.emit("UNIT_POWER", {unitId});
// When player power changes, action bar usability may change
if (block.guid == owner_.playerGuid) {
pendingEvents_.emit("ACTIONBAR_UPDATE_USABLE", {});
pendingEvents_.emit("SPELL_UPDATE_USABLE", {});
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
}
return result;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3d: Apply player stat fields (XP, coinage, combat stats, etc.).
// Shared between CREATE and VALUES — isCreate controls event firing differences.
bool EntityController::applyPlayerStatFields(const std::map<uint16_t, uint32_t>& fields,
const PlayerFieldIndices& pfi,
bool isCreate) {
bool slotsChanged = false;
for (const auto& [key, val] : fields) {
if (key == pfi.xp) {
owner_.playerXp_ = val;
if (!isCreate) {
LOG_DEBUG("XP updated: ", val);
pendingEvents_.emit("PLAYER_XP_UPDATE", {std::to_string(val)});
}
}
else if (key == pfi.nextXp) {
owner_.playerNextLevelXp_ = val;
if (!isCreate) LOG_DEBUG("Next level XP updated: ", val);
}
else if (pfi.restedXp != 0xFFFF && key == pfi.restedXp) {
owner_.playerRestedXp_ = val;
if (!isCreate) pendingEvents_.emit("UPDATE_EXHAUSTION", {});
}
else if (key == pfi.level) {
owner_.serverPlayerLevel_ = val;
if (!isCreate) LOG_DEBUG("Level updated: ", val);
for (auto& ch : owner_.characters) {
if (ch.guid == owner_.playerGuid) { ch.level = val; break; }
}
}
else if (key == pfi.coinage) {
uint64_t oldMoney = owner_.playerMoneyCopper_;
owner_.playerMoneyCopper_ = val;
LOG_DEBUG("Money ", isCreate ? "set from update fields: " : "updated via VALUES: ", val, " copper");
if (val != oldMoney)
pendingEvents_.emit("PLAYER_MONEY", {});
}
else if (pfi.honor != 0xFFFF && key == pfi.honor) {
owner_.playerHonorPoints_ = val;
LOG_DEBUG("Honor points ", isCreate ? "from update fields: " : "updated: ", val);
}
else if (pfi.arena != 0xFFFF && key == pfi.arena) {
owner_.playerArenaPoints_ = val;
LOG_DEBUG("Arena points ", isCreate ? "from update fields: " : "updated: ", val);
}
else if (pfi.armor != 0xFFFF && key == pfi.armor) {
owner_.playerArmorRating_ = static_cast<int32_t>(val);
if (isCreate) LOG_DEBUG("Armor rating from update fields: ", owner_.playerArmorRating_);
}
else if (pfi.armor != 0xFFFF && key > pfi.armor && key <= pfi.armor + 6) {
owner_.playerResistances_[key - pfi.armor - 1] = static_cast<int32_t>(val);
}
else if (pfi.pBytes2 != 0xFFFF && key == pfi.pBytes2) {
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
owner_.inventory.setPurchasedBankBagSlots(bankBagSlots);
// Byte 3 (bits 24-31): REST_STATE
// 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
if (isCreate) {
LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec,
" bankBagSlots=", static_cast<int>(bankBagSlots));
bool wasResting = owner_.isResting_;
owner_.isResting_ = (restStateByte != 0);
if (owner_.isResting_ != wasResting) {
pendingEvents_.emit("UPDATE_EXHAUSTION", {});
pendingEvents_.emit("PLAYER_UPDATE_RESTING", {});
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
} else {
// Byte 0 (bits 0-7): facial hair / piercings
uint8_t facialHair = static_cast<uint8_t>(val & 0xFF);
for (auto& ch : owner_.characters) {
if (ch.guid == owner_.playerGuid) { ch.facialFeatures = facialHair; break; }
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
LOG_DEBUG("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
" bankBagSlots=", static_cast<int>(bankBagSlots),
" facial=", static_cast<int>(facialHair));
owner_.isResting_ = (restStateByte != 0);
if (owner_.appearanceChangedCallback_)
owner_.appearanceChangedCallback_();
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
else if (pfi.chosenTitle != 0xFFFF && key == pfi.chosenTitle) {
owner_.chosenTitleBit_ = static_cast<int32_t>(val);
LOG_DEBUG("PLAYER_CHOSEN_TITLE ", isCreate ? "from update fields: " : "updated: ",
owner_.chosenTitleBit_);
}
// VALUES-only fields: PLAYER_BYTES (appearance) and PLAYER_FLAGS (ghost state)
else if (!isCreate && pfi.pBytes != 0xFFFF && key == pfi.pBytes) {
// PLAYER_BYTES changed (barber shop, polymorph, etc.)
for (auto& ch : owner_.characters) {
if (ch.guid == owner_.playerGuid) { ch.appearanceBytes = val; break; }
}
if (owner_.appearanceChangedCallback_)
owner_.appearanceChangedCallback_();
}
else if (!isCreate && key == pfi.playerFlags) {
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
bool wasGhost = owner_.releasedSpirit_;
bool nowGhost = (val & PLAYER_FLAGS_GHOST) != 0;
if (!wasGhost && nowGhost) {
owner_.releasedSpirit_ = true;
LOG_INFO("Player entered ghost form (PLAYER_FLAGS)");
if (owner_.ghostStateCallback_) owner_.ghostStateCallback_(true);
} else if (wasGhost && !nowGhost) {
owner_.releasedSpirit_ = false;
owner_.playerDead_ = false;
owner_.repopPending_ = false;
owner_.resurrectPending_ = false;
owner_.selfResAvailable_ = false;
owner_.corpseMapId_ = 0; // corpse reclaimed
owner_.corpseGuid_ = 0;
owner_.corpseReclaimAvailableMs_ = 0;
LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)");
pendingEvents_.emit("PLAYER_ALIVE", {});
if (owner_.ghostStateCallback_) owner_.ghostStateCallback_(false);
}
pendingEvents_.emit("PLAYER_FLAGS_CHANGED", {});
}
else if (pfi.meleeAP != 0xFFFF && key == pfi.meleeAP) { owner_.playerMeleeAP_ = static_cast<int32_t>(val); }
else if (pfi.rangedAP != 0xFFFF && key == pfi.rangedAP) { owner_.playerRangedAP_ = static_cast<int32_t>(val); }
else if (pfi.spDmg1 != 0xFFFF && key >= pfi.spDmg1 && key < pfi.spDmg1 + 7) {
owner_.playerSpellDmgBonus_[key - pfi.spDmg1] = static_cast<int32_t>(val);
}
else if (pfi.healBonus != 0xFFFF && key == pfi.healBonus) { owner_.playerHealBonus_ = static_cast<int32_t>(val); }
else if (pfi.blockPct != 0xFFFF && key == pfi.blockPct) { std::memcpy(&owner_.playerBlockPct_, &val, 4); }
else if (pfi.dodgePct != 0xFFFF && key == pfi.dodgePct) { std::memcpy(&owner_.playerDodgePct_, &val, 4); }
else if (pfi.parryPct != 0xFFFF && key == pfi.parryPct) { std::memcpy(&owner_.playerParryPct_, &val, 4); }
else if (pfi.critPct != 0xFFFF && key == pfi.critPct) { std::memcpy(&owner_.playerCritPct_, &val, 4); }
else if (pfi.rangedCritPct != 0xFFFF && key == pfi.rangedCritPct) { std::memcpy(&owner_.playerRangedCritPct_, &val, 4); }
else if (pfi.sCrit1 != 0xFFFF && key >= pfi.sCrit1 && key < pfi.sCrit1 + 7) {
std::memcpy(&owner_.playerSpellCritPct_[key - pfi.sCrit1], &val, 4);
}
else if (pfi.rating1 != 0xFFFF && key >= pfi.rating1 && key < pfi.rating1 + 25) {
owner_.playerCombatRatings_[key - pfi.rating1] = static_cast<int32_t>(val);
}
else {
for (int si = 0; si < 5; ++si) {
if (pfi.stats[si] != 0xFFFF && key == pfi.stats[si]) {
owner_.playerStats_[si] = static_cast<int32_t>(val);
break;
}
}
}
// Do not synthesize quest-log entries from raw update-field slots.
// Slot layouts differ on some classic-family realms and can produce
// phantom "already accepted" quests that block quest acceptance.
}
if (owner_.applyInventoryFields(fields)) slotsChanged = true;
return slotsChanged;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3e: Dispatch entity spawn callbacks for units/players.
// Consolidates player/creature spawn callback invocation from CREATE and VALUES handlers.
// isDead = unitInitiallyDead (CREATE) or computed isDeadNow && !npcDeathNotified (VALUES).
void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType,
const std::shared_ptr<Entity>& entity,
const std::shared_ptr<Unit>& unit,
bool isDead) {
if (objectType == ObjectType::PLAYER && guid == owner_.playerGuid) {
return; // Skip local player — spawned separately via spawnPlayerCharacter()
}
if (objectType == ObjectType::PLAYER) {
if (owner_.playerSpawnCallback_) {
uint8_t race = 0, gender = 0, facial = 0;
uint32_t appearanceBytes = 0;
if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) {
owner_.playerSpawnCallback_(guid, unit->getDisplayId(), race, gender,
appearanceBytes, facial,
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
} else {
LOG_WARNING("[Spawn] PLAYER guid=0x", std::hex, guid, std::dec,
" displayId=", unit->getDisplayId(), " appearance extraction failed — model will not render");
}
}
} else if (owner_.creatureSpawnCallback_) {
LOG_DEBUG("[Spawn] UNIT guid=0x", std::hex, 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;
}
}
owner_.creatureSpawnCallback_(guid, unit->getDisplayId(),
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale);
}
if (isDead && owner_.npcDeathCallback_) {
owner_.npcDeathCallback_(guid);
}
// Query quest giver status for NPCs with questgiver flag (0x02)
if (objectType == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && owner_.socket) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(guid);
owner_.socket->send(qsPkt);
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3g: Track online item/container objects during CREATE.
void EntityController::trackItemOnCreate(const UpdateBlock& block, bool& newItemCreated) {
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY));
auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY));
const uint16_t enchBase = (fieldIndex(UF::ITEM_FIELD_STACK_COUNT) != 0xFFFF)
? static_cast<uint16_t>(fieldIndex(UF::ITEM_FIELD_STACK_COUNT) + 8u) : 0xFFFFu;
auto permEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase) : block.fields.end();
auto tempEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 3u) : block.fields.end();
auto sock1EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 6u) : block.fields.end();
auto sock2EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 9u) : block.fields.end();
auto sock3EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 12u) : block.fields.end();
if (entryIt != block.fields.end() && entryIt->second != 0) {
// Preserve existing info when doing partial updates
GameHandler::OnlineItemInfo info = owner_.onlineItems_.count(block.guid)
? owner_.onlineItems_[block.guid] : GameHandler::OnlineItemInfo{};
info.entry = entryIt->second;
if (stackIt != block.fields.end()) info.stackCount = stackIt->second;
if (durIt != block.fields.end()) info.curDurability = durIt->second;
if (maxDurIt != block.fields.end()) info.maxDurability = maxDurIt->second;
if (permEnchIt != block.fields.end()) info.permanentEnchantId = permEnchIt->second;
if (tempEnchIt != block.fields.end()) info.temporaryEnchantId = tempEnchIt->second;
if (sock1EnchIt != block.fields.end()) info.socketEnchantIds[0] = sock1EnchIt->second;
if (sock2EnchIt != block.fields.end()) info.socketEnchantIds[1] = sock2EnchIt->second;
if (sock3EnchIt != block.fields.end()) info.socketEnchantIds[2] = sock3EnchIt->second;
auto [itemIt, isNew] = owner_.onlineItems_.insert_or_assign(block.guid, info);
if (isNew) newItemCreated = true;
owner_.queryItemInfo(info.entry, block.guid);
}
// Extract container slot GUIDs for bags
if (block.objectType == ObjectType::CONTAINER) {
owner_.extractContainerFields(block.guid, block.fields);
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3g: Update item stack count / durability / enchants for existing items during VALUES.
void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block,
const std::shared_ptr<Entity>& entity) {
bool inventoryChanged = false;
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
const uint16_t itemDurField = fieldIndex(UF::ITEM_FIELD_DURABILITY);
const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY);
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
// ITEM_FIELD_ENCHANTMENT starts 8 fields after ITEM_FIELD_STACK_COUNT (fixed offset
// across all expansions: +DURATION, +5×SPELL_CHARGES, +FLAGS = +8).
// Slot 0 = permanent (field +0), slot 1 = temp (+3), slots 2-4 = sockets (+6,+9,+12).
const uint16_t itemEnchBase = (itemStackField != 0xFFFF) ? (itemStackField + 8u) : 0xFFFF;
const uint16_t itemPermEnchField = itemEnchBase;
const uint16_t itemTempEnchField = (itemEnchBase != 0xFFFF) ? (itemEnchBase + 3u) : 0xFFFF;
const uint16_t itemSock1EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 6u) : 0xFFFF;
const uint16_t itemSock2EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 9u) : 0xFFFF;
const uint16_t itemSock3EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 12u) : 0xFFFF;
auto it = owner_.onlineItems_.find(block.guid);
bool isItemInInventory = (it != owner_.onlineItems_.end());
for (const auto& [key, val] : block.fields) {
if (key == itemStackField && isItemInInventory) {
if (it->second.stackCount != val) {
it->second.stackCount = val;
inventoryChanged = true;
}
} else if (key == itemDurField && isItemInInventory) {
if (it->second.curDurability != val) {
const uint32_t prevDur = it->second.curDurability;
it->second.curDurability = val;
inventoryChanged = true;
// Warn once when durability drops below 20% for an equipped item.
const uint32_t maxDur = it->second.maxDurability;
if (maxDur > 0 && val < maxDur / 5u && prevDur >= maxDur / 5u) {
// Check if this item is in an equip slot (not bag inventory).
bool isEquipped = false;
for (uint64_t slotGuid : owner_.equipSlotGuids_) {
if (slotGuid == block.guid) { isEquipped = true; break; }
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
if (isEquipped) {
std::string itemName;
const auto* info = owner_.getItemInfo(it->second.entry);
if (info) itemName = info->name;
char buf[128];
if (!itemName.empty())
std::snprintf(buf, sizeof(buf), "%s is about to break!", itemName.c_str());
else
std::snprintf(buf, sizeof(buf), "An equipped item is about to break!");
owner_.addUIError(buf);
owner_.addSystemChatMessage(buf);
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
} else if (key == itemMaxDurField && isItemInInventory) {
if (it->second.maxDurability != val) {
it->second.maxDurability = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemPermEnchField != 0xFFFF && key == itemPermEnchField) {
if (it->second.permanentEnchantId != val) {
it->second.permanentEnchantId = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemTempEnchField != 0xFFFF && key == itemTempEnchField) {
if (it->second.temporaryEnchantId != val) {
it->second.temporaryEnchantId = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock1EnchField != 0xFFFF && key == itemSock1EnchField) {
if (it->second.socketEnchantIds[0] != val) {
it->second.socketEnchantIds[0] = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock2EnchField != 0xFFFF && key == itemSock2EnchField) {
if (it->second.socketEnchantIds[1] != val) {
it->second.socketEnchantIds[1] = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock3EnchField != 0xFFFF && key == itemSock3EnchField) {
if (it->second.socketEnchantIds[2] != val) {
it->second.socketEnchantIds[2] = val;
inventoryChanged = true;
}
}
}
// Update container slot GUIDs on bag content changes
if (entity->getType() == ObjectType::CONTAINER) {
for (const auto& [key, _] : block.fields) {
if ((containerNumSlotsField != 0xFFFF && key == containerNumSlotsField) ||
(containerSlot1Field != 0xFFFF && key >= containerSlot1Field && key < containerSlot1Field + 72)) {
inventoryChanged = true;
break;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
owner_.extractContainerFields(block.guid, block.fields);
}
if (inventoryChanged) {
owner_.rebuildOnlineInventory();
pendingEvents_.emit("BAG_UPDATE", {});
pendingEvents_.emit("UNIT_INVENTORY_CHANGED", {"player"});
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// ============================================================
// Phase 5: Object-type handler struct definitions
// ============================================================
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
struct EntityController::UnitTypeHandler : EntityController::IObjectTypeHandler {
EntityController& ctl_;
explicit UnitTypeHandler(EntityController& c) : ctl_(c) {}
void onCreate(const UpdateBlock& block, std::shared_ptr<Entity>& entity, bool&) override { ctl_.onCreateUnit(block, entity); }
void onValuesUpdate(const UpdateBlock& block, std::shared_ptr<Entity>& entity) override { ctl_.onValuesUpdateUnit(block, entity); }
};
struct EntityController::PlayerTypeHandler : EntityController::IObjectTypeHandler {
EntityController& ctl_;
explicit PlayerTypeHandler(EntityController& c) : ctl_(c) {}
void onCreate(const UpdateBlock& block, std::shared_ptr<Entity>& entity, bool&) override { ctl_.onCreatePlayer(block, entity); }
void onValuesUpdate(const UpdateBlock& block, std::shared_ptr<Entity>& entity) override { ctl_.onValuesUpdatePlayer(block, entity); }
};
struct EntityController::GameObjectTypeHandler : EntityController::IObjectTypeHandler {
EntityController& ctl_;
explicit GameObjectTypeHandler(EntityController& c) : ctl_(c) {}
void onCreate(const UpdateBlock& block, std::shared_ptr<Entity>& entity, bool&) override { ctl_.onCreateGameObject(block, entity); }
void onValuesUpdate(const UpdateBlock& block, std::shared_ptr<Entity>& entity) override { ctl_.onValuesUpdateGameObject(block, entity); }
};
struct EntityController::ItemTypeHandler : EntityController::IObjectTypeHandler {
EntityController& ctl_;
explicit ItemTypeHandler(EntityController& c) : ctl_(c) {}
void onCreate(const UpdateBlock& block, std::shared_ptr<Entity>& entity, bool& newItemCreated) override { ctl_.onCreateItem(block, newItemCreated); }
void onValuesUpdate(const UpdateBlock& block, std::shared_ptr<Entity>& entity) override { ctl_.onValuesUpdateItem(block, entity); }
};
struct EntityController::CorpseTypeHandler : EntityController::IObjectTypeHandler {
EntityController& ctl_;
explicit CorpseTypeHandler(EntityController& c) : ctl_(c) {}
void onCreate(const UpdateBlock& block, std::shared_ptr<Entity>& entity, bool&) override { ctl_.onCreateCorpse(block); }
};
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// ============================================================
// Phase 5: Handler registry infrastructure
// ============================================================
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
void EntityController::initTypeHandlers() {
typeHandlers_[static_cast<uint8_t>(ObjectType::UNIT)] = std::make_unique<UnitTypeHandler>(*this);
typeHandlers_[static_cast<uint8_t>(ObjectType::PLAYER)] = std::make_unique<PlayerTypeHandler>(*this);
typeHandlers_[static_cast<uint8_t>(ObjectType::GAMEOBJECT)] = std::make_unique<GameObjectTypeHandler>(*this);
typeHandlers_[static_cast<uint8_t>(ObjectType::ITEM)] = std::make_unique<ItemTypeHandler>(*this);
typeHandlers_[static_cast<uint8_t>(ObjectType::CONTAINER)] = std::make_unique<ItemTypeHandler>(*this);
typeHandlers_[static_cast<uint8_t>(ObjectType::CORPSE)] = std::make_unique<CorpseTypeHandler>(*this);
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
EntityController::IObjectTypeHandler* EntityController::getTypeHandler(ObjectType type) const {
auto it = typeHandlers_.find(static_cast<uint8_t>(type));
return it != typeHandlers_.end() ? it->second.get() : nullptr;
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// ============================================================
// Phase 6: Deferred event bus flush
// ============================================================
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
void EntityController::flushPendingEvents() {
for (const auto& [name, args] : pendingEvents_.events) {
owner_.fireAddonEvent(name, args);
}
pendingEvents_.clear();
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// ============================================================
// Phase 5: Type-specific CREATE handlers
// ============================================================
void EntityController::onCreateUnit(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
// Name query for creatures
auto it = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
if (it != block.fields.end() && it->second != 0) {
auto unit = std::static_pointer_cast<Unit>(entity);
unit->setEntry(it->second);
std::string cached = getCachedCreatureName(it->second);
if (!cached.empty()) {
unit->setName(cached);
}
queryCreatureInfo(it->second, block.guid);
}
// Unit fields
auto unit = std::static_pointer_cast<Unit>(entity);
UnitFieldIndices ufi = UnitFieldIndices::resolve();
bool unitInitiallyDead = applyUnitFieldsOnCreate(block, unit, ufi);
// Hostility
unit->setHostile(owner_.isHostileFaction(unit->getFactionTemplate()));
// Spawn dispatch
if (unit->getDisplayId() == 0) {
LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec,
" has displayId=0 — no spawn (entry=", unit->getEntry(),
" at ", unit->getX(), ",", unit->getY(), ",", unit->getZ(), ")");
}
if (unit->getDisplayId() != 0) {
dispatchEntitySpawn(block.guid, block.objectType, entity, unit, unitInitiallyDead);
if (block.hasMovement && block.moveFlags != 0 && owner_.unitMoveFlagsCallback_ &&
block.guid != owner_.playerGuid) {
owner_.unitMoveFlagsCallback_(block.guid, block.moveFlags);
}
}
}
void EntityController::onCreatePlayer(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
static const bool kVerboseUpdateObject = envFlagEnabled("WOWEE_LOG_UPDATE_OBJECT_VERBOSE", false);
// For the local player, capture the full initial field state
if (block.guid == owner_.playerGuid) {
owner_.lastPlayerFields_ = entity->getFields();
owner_.maybeDetectVisibleItemLayout();
}
// Name query + visible items
queryPlayerName(block.guid);
if (block.guid != owner_.playerGuid) {
owner_.updateOtherPlayerVisibleItems(block.guid, entity->getFields());
}
// Unit fields (PLAYER is a unit)
auto unit = std::static_pointer_cast<Unit>(entity);
UnitFieldIndices ufi = UnitFieldIndices::resolve();
bool unitInitiallyDead = applyUnitFieldsOnCreate(block, unit, ufi);
// Self-player post-unit-field handling
if (block.guid == owner_.playerGuid) {
constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100;
if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !owner_.onTaxiFlight_ && owner_.taxiLandingCooldown_ <= 0.0f) {
owner_.onTaxiFlight_ = true;
owner_.taxiStartGrace_ = std::max(owner_.taxiStartGrace_, 2.0f);
owner_.sanitizeMovementForTaxi();
if (owner_.movementHandler_) owner_.movementHandler_->applyTaxiMountForCurrentNode();
}
}
if (block.guid == owner_.playerGuid &&
(unit->getDynamicFlags() & 0x0008 /*UNIT_DYNFLAG_DEAD*/) != 0) {
owner_.playerDead_ = true;
LOG_INFO("Player logged in dead (dynamic flags)");
}
// Detect ghost state on login via PLAYER_FLAGS
if (block.guid == owner_.playerGuid) {
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
auto pfIt = block.fields.find(fieldIndex(UF::PLAYER_FLAGS));
if (pfIt != block.fields.end() && (pfIt->second & PLAYER_FLAGS_GHOST) != 0) {
owner_.releasedSpirit_ = true;
owner_.playerDead_ = true;
LOG_INFO("Player logged in as ghost (PLAYER_FLAGS)");
if (owner_.ghostStateCallback_) owner_.ghostStateCallback_(true);
// Query corpse position so minimap marker is accurate on reconnect
if (owner_.socket) {
network::Packet cq(wireOpcode(Opcode::MSG_CORPSE_QUERY));
owner_.socket->send(cq);
}
}
}
// 3f: Classic aura sync on initial object create
if (block.guid == owner_.playerGuid) {
syncClassicAurasFromFields(entity);
}
// Hostility
unit->setHostile(owner_.isHostileFaction(unit->getFactionTemplate()));
// Spawn dispatch
if (unit->getDisplayId() != 0) {
dispatchEntitySpawn(block.guid, block.objectType, entity, unit, unitInitiallyDead);
if (block.hasMovement && block.moveFlags != 0 && owner_.unitMoveFlagsCallback_ &&
block.guid != owner_.playerGuid) {
owner_.unitMoveFlagsCallback_(block.guid, block.moveFlags);
}
}
// 3d: Player stat fields (self only)
if (block.guid == owner_.playerGuid) {
// Auto-detect coinage index using the previous snapshot vs this full snapshot.
maybeDetectCoinageIndex(owner_.lastPlayerFields_, block.fields);
owner_.lastPlayerFields_ = block.fields;
owner_.detectInventorySlotBases(block.fields);
if (kVerboseUpdateObject) {
uint16_t maxField = 0;
for (const auto& [key, _val] : block.fields) {
if (key > maxField) maxField = key;
}
LOG_INFO("Player update with ", block.fields.size(),
" fields (max index=", maxField, ")");
}
PlayerFieldIndices pfi = PlayerFieldIndices::resolve();
bool slotsChanged = applyPlayerStatFields(block.fields, pfi, true);
if (slotsChanged) owner_.rebuildOnlineInventory();
owner_.maybeDetectVisibleItemLayout();
owner_.extractSkillFields(owner_.lastPlayerFields_);
owner_.extractExploredZoneFields(owner_.lastPlayerFields_);
owner_.applyQuestStateFromFields(owner_.lastPlayerFields_);
}
}
void EntityController::onCreateGameObject(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
auto go = std::static_pointer_cast<GameObject>(entity);
auto itDisp = block.fields.find(fieldIndex(UF::GAMEOBJECT_DISPLAYID));
if (itDisp != block.fields.end()) {
go->setDisplayId(itDisp->second);
}
auto itEntry = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
if (itEntry != block.fields.end() && itEntry->second != 0) {
go->setEntry(itEntry->second);
auto cacheIt = gameObjectInfoCache_.find(itEntry->second);
if (cacheIt != gameObjectInfoCache_.end()) {
go->setName(cacheIt->second.name);
}
queryGameObjectInfo(itEntry->second, block.guid);
}
// Detect transport GameObjects via UPDATEFLAG_TRANSPORT (0x0002)
LOG_DEBUG("GameObject CREATE: guid=0x", std::hex, block.guid, std::dec,
" entry=", go->getEntry(), " displayId=", go->getDisplayId(),
" updateFlags=0x", std::hex, block.updateFlags, std::dec,
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
if (block.updateFlags & 0x0002) {
transportGuids_.insert(block.guid);
LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec,
" entry=", go->getEntry(),
" displayId=", go->getDisplayId(),
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
// Note: TransportSpawnCallback will be invoked from Application after WMO instance is created
}
if (go->getDisplayId() != 0 && owner_.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;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
}
owner_.gameObjectSpawnCallback_(block.guid, go->getEntry(), go->getDisplayId(),
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) && owner_.transportMoveCallback_) {
serverUpdatedTransportGuids_.insert(block.guid);
owner_.transportMoveCallback_(block.guid,
go->getX(), go->getY(), go->getZ(), go->getOrientation());
}
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
void EntityController::onCreateItem(const UpdateBlock& block, bool& newItemCreated) {
trackItemOnCreate(block, newItemCreated);
}
void EntityController::onCreateCorpse(const UpdateBlock& block) {
// Detect player's own corpse object so we have the position even when
// SMSG_DEATH_RELEASE_LOC hasn't been received (e.g. login as ghost).
if (block.hasMovement) {
// CORPSE_FIELD_OWNER is at index 6 (uint64, low word at 6, high at 7)
uint16_t ownerLowIdx = 6;
auto ownerLowIt = block.fields.find(ownerLowIdx);
uint32_t ownerLow = (ownerLowIt != block.fields.end()) ? ownerLowIt->second : 0;
auto ownerHighIt = block.fields.find(ownerLowIdx + 1);
uint32_t ownerHigh = (ownerHighIt != block.fields.end()) ? ownerHighIt->second : 0;
uint64_t ownerGuid = (static_cast<uint64_t>(ownerHigh) << 32) | ownerLow;
if (ownerGuid == owner_.playerGuid || ownerLow == static_cast<uint32_t>(owner_.playerGuid)) {
// Server coords from movement block
owner_.corpseGuid_ = block.guid;
owner_.corpseX_ = block.x;
owner_.corpseY_ = block.y;
owner_.corpseZ_ = block.z;
owner_.corpseMapId_ = owner_.currentMapId_;
LOG_INFO("Corpse object detected: guid=0x", std::hex, owner_.corpseGuid_, std::dec,
" server=(", block.x, ", ", block.y, ", ", block.z,
") map=", owner_.corpseMapId_);
}
}
}
// ============================================================
// Phase 5: Type-specific VALUES UPDATE handlers
// ============================================================
void EntityController::onValuesUpdateUnit(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
auto unit = std::static_pointer_cast<Unit>(entity);
UnitFieldIndices ufi = UnitFieldIndices::resolve();
UnitFieldUpdateResult result = applyUnitFieldsOnUpdate(block, entity, unit, ufi);
// Display ID changed — re-spawn/model-change notification
if (result.displayIdChanged && unit->getDisplayId() != 0 &&
unit->getDisplayId() != result.oldDisplayId) {
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001;
bool isDeadNow = (unit->getHealth() == 0) ||
((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0);
dispatchEntitySpawn(block.guid, entity->getType(), entity, unit,
isDeadNow && !result.npcDeathNotified);
if (owner_.addonEventCallback_) {
std::string uid;
if (block.guid == owner_.targetGuid) uid = "target";
else if (block.guid == owner_.focusGuid) uid = "focus";
else if (block.guid == owner_.petGuid_) uid = "pet";
if (!uid.empty())
pendingEvents_.emit("UNIT_MODEL_CHANGED", {uid});
}
}
}
void EntityController::onValuesUpdatePlayer(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
// Other player visible items
if (block.guid != owner_.playerGuid) {
owner_.updateOtherPlayerVisibleItems(block.guid, entity->getFields());
}
// Unit field update (player IS a unit)
auto unit = std::static_pointer_cast<Unit>(entity);
UnitFieldIndices ufi = UnitFieldIndices::resolve();
UnitFieldUpdateResult result = applyUnitFieldsOnUpdate(block, entity, unit, ufi);
// 3f: Classic aura sync from UNIT_FIELD_AURAS when those fields are updated
if (block.guid == owner_.playerGuid) {
syncClassicAurasFromFields(entity);
}
// 3e: Display ID changed — re-spawn/model-change
if (result.displayIdChanged && unit->getDisplayId() != 0 &&
unit->getDisplayId() != result.oldDisplayId) {
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001;
bool isDeadNow = (unit->getHealth() == 0) ||
((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0);
dispatchEntitySpawn(block.guid, entity->getType(), entity, unit,
isDeadNow && !result.npcDeathNotified);
if (owner_.addonEventCallback_) {
std::string uid;
if (block.guid == owner_.targetGuid) uid = "target";
else if (block.guid == owner_.focusGuid) uid = "focus";
else if (block.guid == owner_.petGuid_) uid = "pet";
if (!uid.empty())
pendingEvents_.emit("UNIT_MODEL_CHANGED", {uid});
}
}
// 3d: Self-player stat/inventory/quest field updates
if (block.guid == owner_.playerGuid) {
const bool needCoinageDetectSnapshot =
(owner_.pendingMoneyDelta_ != 0 && owner_.pendingMoneyDeltaTimer_ > 0.0f);
std::map<uint16_t, uint32_t> oldFieldsSnapshot;
if (needCoinageDetectSnapshot) {
oldFieldsSnapshot = owner_.lastPlayerFields_;
}
if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
owner_.serverRunSpeed_ = block.runSpeed;
// Some server dismount paths update run speed without updating mount display field.
if (!owner_.onTaxiFlight_ && !owner_.taxiMountActive_ &&
owner_.currentMountDisplayId_ != 0 && block.runSpeed <= 8.5f) {
LOG_INFO("Auto-clearing mount from movement speed update: speed=", block.runSpeed,
" displayId=", owner_.currentMountDisplayId_);
owner_.currentMountDisplayId_ = 0;
if (owner_.mountCallback_) {
owner_.mountCallback_(0);
}
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
}
auto mergeHint = owner_.lastPlayerFields_.end();
for (const auto& [key, val] : block.fields) {
mergeHint = owner_.lastPlayerFields_.insert_or_assign(mergeHint, key, val);
}
if (needCoinageDetectSnapshot) {
maybeDetectCoinageIndex(oldFieldsSnapshot, owner_.lastPlayerFields_);
}
owner_.maybeDetectVisibleItemLayout();
owner_.detectInventorySlotBases(block.fields);
PlayerFieldIndices pfi = PlayerFieldIndices::resolve();
bool slotsChanged = applyPlayerStatFields(block.fields, pfi, false);
if (slotsChanged) {
owner_.rebuildOnlineInventory();
pendingEvents_.emit("PLAYER_EQUIPMENT_CHANGED", {});
}
owner_.extractSkillFields(owner_.lastPlayerFields_);
owner_.extractExploredZoneFields(owner_.lastPlayerFields_);
owner_.applyQuestStateFromFields(owner_.lastPlayerFields_);
}
}
void EntityController::onValuesUpdateItem(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
updateItemOnValuesUpdate(block, entity);
}
void EntityController::onValuesUpdateGameObject(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
if (block.hasMovement) {
if (transportGuids_.count(block.guid) && owner_.transportMoveCallback_) {
serverUpdatedTransportGuids_.insert(block.guid);
owner_.transportMoveCallback_(block.guid, entity->getX(), entity->getY(),
entity->getZ(), entity->getOrientation());
} else if (owner_.gameObjectMoveCallback_) {
owner_.gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(),
entity->getZ(), entity->getOrientation());
}
}
}
// ============================================================
// Phase 2: Update type handlers (refactored with Phase 5 dispatch)
// ============================================================
void EntityController::handleCreateObject(const UpdateBlock& block, bool& newItemCreated) {
pendingEvents_.clear();
// 3a: Create entity from block type
std::shared_ptr<Entity> entity = createEntityFromBlock(block);
// Set position from movement block (server → canonical)
if (block.hasMovement) {
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
float oCanonical = core::coords::serverToCanonicalYaw(block.orientation);
entity->setPosition(pos.x, pos.y, pos.z, oCanonical);
LOG_DEBUG(" Position: (", pos.x, ", ", pos.y, ", ", pos.z, ")");
if (block.guid == owner_.playerGuid && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
owner_.serverRunSpeed_ = block.runSpeed;
}
// 3b: Track player-on-transport state
if (block.guid == owner_.playerGuid) {
applyPlayerTransportState(block, entity, pos, oCanonical, false);
}
// 3i: Track transport-relative children so they follow parent transport motion.
updateNonPlayerTransportAttachment(block, entity, block.objectType);
}
// Set fields
for (const auto& field : block.fields) {
entity->setField(field.first, field.second);
}
// Add to manager
entityManager.addEntity(block.guid, entity);
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// Phase 5: Dispatch to type-specific handler
auto* handler = getTypeHandler(block.objectType);
if (handler) handler->onCreate(block, entity, newItemCreated);
flushPendingEvents();
}
void EntityController::handleValuesUpdate(const UpdateBlock& block, bool& /*newItemCreated*/) {
auto entity = entityManager.getEntity(block.guid);
if (!entity) return;
pendingEvents_.clear();
// Position update (common)
if (block.hasMovement) {
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
float oCanonical = core::coords::serverToCanonicalYaw(block.orientation);
entity->setPosition(pos.x, pos.y, pos.z, oCanonical);
updateNonPlayerTransportAttachment(block, entity, entity->getType());
}
// Set fields (common)
for (const auto& field : block.fields) {
entity->setField(field.first, field.second);
}
// Phase 5: Dispatch to type-specific handler
auto* handler = getTypeHandler(entity->getType());
if (handler) handler->onValuesUpdate(block, entity);
flushPendingEvents();
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
}
void EntityController::handleMovementUpdate(const UpdateBlock& block) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// Diagnostic: Log if we receive MOVEMENT blocks for transports
if (transportGuids_.count(block.guid)) {
LOG_INFO("MOVEMENT update for transport 0x", std::hex, block.guid, std::dec,
" pos=(", block.x, ", ", block.y, ", ", block.z, ")");
}
// Update entity position (server → canonical)
auto entity = entityManager.getEntity(block.guid);
if (entity) {
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
float oCanonical = core::coords::serverToCanonicalYaw(block.orientation);
entity->setPosition(pos.x, pos.y, pos.z, oCanonical);
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
updateNonPlayerTransportAttachment(block, entity, entity->getType());
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
// 3b: Track player-on-transport state from MOVEMENT updates
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (block.guid == owner_.playerGuid) {
owner_.movementInfo.orientation = oCanonical;
applyPlayerTransportState(block, entity, pos, oCanonical, true);
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
}
// Fire transport move callback if this is a known transport
if (transportGuids_.count(block.guid) && owner_.transportMoveCallback_) {
serverUpdatedTransportGuids_.insert(block.guid);
owner_.transportMoveCallback_(block.guid, pos.x, pos.y, pos.z, oCanonical);
}
// Fire move callback for non-transport gameobjects.
if (entity->getType() == ObjectType::GAMEOBJECT &&
transportGuids_.count(block.guid) == 0 &&
owner_.gameObjectMoveCallback_) {
owner_.gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(),
entity->getZ(), entity->getOrientation());
}
// Fire move callback for non-player units (creatures).
// SMSG_MONSTER_MOVE handles smooth interpolated movement, but many
// servers (especially vanilla/Turtle WoW) communicate NPC positions
// via MOVEMENT blocks instead. Use duration=0 for an instant snap.
if (block.guid != owner_.playerGuid &&
entity->getType() == ObjectType::UNIT &&
transportGuids_.count(block.guid) == 0 &&
owner_.creatureMoveCallback_) {
owner_.creatureMoveCallback_(block.guid, pos.x, pos.y, pos.z, 0);
}
} else {
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
}
}
void EntityController::finalizeUpdateObjectBatch(bool newItemCreated) {
owner_.tabCycleStale = true;
// Entity count logging disabled
// Deferred rebuild: if new item objects were created in this packet, rebuild
// owner_.inventory so that slot GUIDs updated earlier in the same packet can resolve.
if (newItemCreated) {
owner_.rebuildOnlineInventory();
}
// Late owner_.inventory base detection once items are known
if (owner_.playerGuid != 0 && owner_.invSlotBase_ < 0 && !owner_.lastPlayerFields_.empty() && !owner_.onlineItems_.empty()) {
owner_.detectInventorySlotBases(owner_.lastPlayerFields_);
if (owner_.invSlotBase_ >= 0) {
if (owner_.applyInventoryFields(owner_.lastPlayerFields_)) {
owner_.rebuildOnlineInventory();
}
}
}
}
void EntityController::handleCompressedUpdateObject(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_COMPRESSED_UPDATE_OBJECT, packet size: ", packet.getSize());
// First 4 bytes = decompressed size
if (packet.getSize() < 4) {
LOG_WARNING("SMSG_COMPRESSED_UPDATE_OBJECT too small");
return;
}
uint32_t decompressedSize = packet.readUInt32();
LOG_DEBUG(" Decompressed size: ", decompressedSize);
// Capital cities and large raids can produce very large update packets.
// The real WoW client handles up to ~10MB; 5MB covers all practical cases.
if (decompressedSize == 0 || decompressedSize > 5 * 1024 * 1024) {
LOG_WARNING("Invalid decompressed size: ", decompressedSize);
return;
}
// Remaining data is zlib compressed
size_t compressedSize = packet.getRemainingSize();
const uint8_t* compressedData = packet.getData().data() + packet.getReadPos();
// Decompress
std::vector<uint8_t> decompressed(decompressedSize);
uLongf destLen = decompressedSize;
int ret = uncompress(decompressed.data(), &destLen, compressedData, compressedSize);
if (ret != Z_OK) {
LOG_WARNING("Failed to decompress UPDATE_OBJECT: zlib error ", ret);
return;
}
// Create packet from decompressed data and parse it
network::Packet decompressedPacket(wireOpcode(Opcode::SMSG_UPDATE_OBJECT), decompressed);
handleUpdateObject(decompressedPacket);
}
void EntityController::handleDestroyObject(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_DESTROY_OBJECT");
DestroyObjectData data;
if (!DestroyObjectParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_DESTROY_OBJECT");
return;
}
// Remove entity
if (entityManager.hasEntity(data.guid)) {
if (transportGuids_.count(data.guid) > 0) {
const bool playerAboardNow = (owner_.playerTransportGuid_ == data.guid);
const bool stickyAboard = (owner_.playerTransportStickyGuid_ == data.guid && owner_.playerTransportStickyTimer_ > 0.0f);
const bool movementSaysAboard = (owner_.movementInfo.transportGuid == data.guid);
if (playerAboardNow || stickyAboard || movementSaysAboard) {
serverUpdatedTransportGuids_.erase(data.guid);
LOG_INFO("Preserving in-use transport on destroy: 0x", std::hex, data.guid, std::dec,
" now=", playerAboardNow,
" sticky=", stickyAboard,
" movement=", movementSaysAboard);
return;
}
}
// Mirror out-of-range handling: invoke render-layer despawn callbacks before entity removal.
auto entity = entityManager.getEntity(data.guid);
if (entity) {
if (entity->getType() == ObjectType::UNIT && owner_.creatureDespawnCallback_) {
owner_.creatureDespawnCallback_(data.guid);
} else if (entity->getType() == ObjectType::PLAYER && owner_.playerDespawnCallback_) {
// Player entities also need renderer cleanup on DESTROY_OBJECT, not just out-of-range.
owner_.playerDespawnCallback_(data.guid);
owner_.otherPlayerVisibleItemEntries_.erase(data.guid);
owner_.otherPlayerVisibleDirty_.erase(data.guid);
owner_.otherPlayerMoveTimeMs_.erase(data.guid);
owner_.inspectedPlayerItemEntries_.erase(data.guid);
owner_.pendingAutoInspect_.erase(data.guid);
pendingNameQueries.erase(data.guid);
} else if (entity->getType() == ObjectType::GAMEOBJECT && owner_.gameObjectDespawnCallback_) {
owner_.gameObjectDespawnCallback_(data.guid);
}
}
if (transportGuids_.count(data.guid) > 0) {
transportGuids_.erase(data.guid);
serverUpdatedTransportGuids_.erase(data.guid);
if (owner_.playerTransportGuid_ == data.guid) {
owner_.clearPlayerTransport();
}
}
owner_.clearTransportAttachment(data.guid);
entityManager.removeEntity(data.guid);
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
" (", (data.isDeath ? "death" : "despawn"), ")");
} else {
LOG_DEBUG("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec);
}
// Clean up auto-attack and target if destroyed entity was our target
if (owner_.combatHandler_ && data.guid == owner_.combatHandler_->getAutoAttackTargetGuid()) {
owner_.stopAutoAttack();
}
if (data.guid == owner_.targetGuid) {
owner_.targetGuid = 0;
}
if (owner_.combatHandler_) owner_.combatHandler_->removeHostileAttacker(data.guid);
// Remove online item/container tracking
owner_.containerContents_.erase(data.guid);
if (owner_.onlineItems_.erase(data.guid)) {
owner_.rebuildOnlineInventory();
}
// Clean up quest giver status
owner_.npcQuestStatus_.erase(data.guid);
// Remove combat text entries referencing the destroyed entity so floating
// damage numbers don't linger after the source/target despawns.
if (owner_.combatHandler_) owner_.combatHandler_->removeCombatTextForGuid(data.guid);
// Clean up unit cast owner_.state (cast bar) for the destroyed unit
if (owner_.spellHandler_) owner_.spellHandler_->unitCastStates_.erase(data.guid);
// Clean up cached auras
if (owner_.spellHandler_) owner_.spellHandler_->unitAurasCache_.erase(data.guid);
owner_.tabCycleStale = true;
}
// Name Queries
// ============================================================
void EntityController::queryPlayerName(uint64_t guid) {
// If already cached, apply the name to the entity (handles entity recreation after
// moving out/in range — the entity object is new but the cached name is valid).
auto cacheIt = playerNameCache.find(guid);
if (cacheIt != playerNameCache.end()) {
auto entity = entityManager.getEntity(guid);
if (entity && entity->getType() == ObjectType::PLAYER) {
auto player = std::static_pointer_cast<Player>(entity);
if (player->getName().empty()) {
player->setName(cacheIt->second);
}
}
return;
}
if (pendingNameQueries.count(guid)) return;
if (!owner_.isInWorld()) {
LOG_INFO("queryPlayerName: skipped guid=0x", std::hex, guid, std::dec,
" owner_.state=", worldStateName(owner_.state), " owner_.socket=", (owner_.socket ? "yes" : "no"));
return;
}
LOG_INFO("queryPlayerName: sending CMSG_NAME_QUERY for guid=0x", std::hex, guid, std::dec);
pendingNameQueries.insert(guid);
auto packet = NameQueryPacket::build(guid);
owner_.socket->send(packet);
}
void EntityController::queryCreatureInfo(uint32_t entry, uint64_t guid) {
if (creatureInfoCache.count(entry) || pendingCreatureQueries.count(entry)) return;
if (!owner_.isInWorld()) return;
pendingCreatureQueries.insert(entry);
auto packet = CreatureQueryPacket::build(entry, guid);
owner_.socket->send(packet);
}
void EntityController::queryGameObjectInfo(uint32_t entry, uint64_t guid) {
if (gameObjectInfoCache_.count(entry) || pendingGameObjectQueries_.count(entry)) return;
if (!owner_.isInWorld()) return;
pendingGameObjectQueries_.insert(entry);
auto packet = GameObjectQueryPacket::build(entry, guid);
owner_.socket->send(packet);
}
std::string EntityController::getCachedPlayerName(uint64_t guid) const {
return std::string(lookupName(guid));
}
std::string EntityController::getCachedCreatureName(uint32_t entry) const {
auto it = creatureInfoCache.find(entry);
return (it != creatureInfoCache.end()) ? it->second.name : "";
}
void EntityController::handleNameQueryResponse(network::Packet& packet) {
NameQueryResponseData data;
if (!owner_.packetParsers_ || !owner_.packetParsers_->parseNameQueryResponse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_NAME_QUERY_RESPONSE (size=", packet.getSize(), ")");
return;
}
pendingNameQueries.erase(data.guid);
LOG_INFO("Name query response: guid=0x", std::hex, data.guid, std::dec,
" found=", static_cast<int>(data.found), " name='", data.name, "'",
" race=", static_cast<int>(data.race), " class=", static_cast<int>(data.classId));
if (data.isValid()) {
playerNameCache[data.guid] = data.name;
// Cache class/race from name query for UnitClass/UnitRace fallback
if (data.classId != 0 || data.race != 0) {
playerClassRaceCache_[data.guid] = {data.classId, data.race};
}
// Update entity name
auto entity = entityManager.getEntity(data.guid);
if (entity && entity->getType() == ObjectType::PLAYER) {
auto player = std::static_pointer_cast<Player>(entity);
player->setName(data.name);
}
// Backfill chat history entries that arrived before we knew the name.
if (owner_.chatHandler_) {
for (auto& msg : owner_.chatHandler_->getChatHistory()) {
if (msg.senderGuid == data.guid && msg.senderName.empty()) {
msg.senderName = data.name;
}
}
}
// Backfill mail inbox sender names
for (auto& mail : owner_.mailInbox_) {
if (mail.messageType == 0 && mail.senderGuid == data.guid) {
mail.senderName = data.name;
}
}
// Backfill friend list: if this GUID came from a friend list packet,
// register the name in owner_.friendsCache now that we know it.
if (owner_.friendGuids_.count(data.guid)) {
owner_.friendsCache[data.name] = data.guid;
}
// Fire UNIT_NAME_UPDATE so nameplate/unit frame addons know the name is available
if (owner_.addonEventCallback_) {
std::string unitId;
if (data.guid == owner_.targetGuid) unitId = "target";
else if (data.guid == owner_.focusGuid) unitId = "focus";
else if (data.guid == owner_.playerGuid) unitId = "player";
if (!unitId.empty())
owner_.fireAddonEvent("UNIT_NAME_UPDATE", {unitId});
}
}
}
void EntityController::handleCreatureQueryResponse(network::Packet& packet) {
CreatureQueryResponseData data;
if (!owner_.packetParsers_->parseCreatureQueryResponse(packet, data)) return;
pendingCreatureQueries.erase(data.entry);
if (data.isValid()) {
creatureInfoCache[data.entry] = data;
// Update all unit entities with this entry
for (auto& [guid, entity] : entityManager.getEntities()) {
if (entity->getType() == ObjectType::UNIT) {
auto unit = std::static_pointer_cast<Unit>(entity);
if (unit->getEntry() == data.entry) {
unit->setName(data.name);
}
}
}
}
}
// ============================================================
// GameObject Query
// ============================================================
void EntityController::handleGameObjectQueryResponse(network::Packet& packet) {
GameObjectQueryResponseData data;
bool ok = owner_.packetParsers_ ? owner_.packetParsers_->parseGameObjectQueryResponse(packet, data)
: GameObjectQueryResponseParser::parse(packet, data);
if (!ok) return;
pendingGameObjectQueries_.erase(data.entry);
if (data.isValid()) {
gameObjectInfoCache_[data.entry] = data;
// Update all gameobject entities with this entry
for (auto& [guid, entity] : entityManager.getEntities()) {
if (entity->getType() == ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<GameObject>(entity);
if (go->getEntry() == data.entry) {
go->setName(data.name);
}
}
}
// MO_TRANSPORT (type 15): assign TaxiPathNode path if available
if (data.type == 15 && data.hasData && data.data[0] != 0 && owner_.transportManager_) {
uint32_t taxiPathId = data.data[0];
if (owner_.transportManager_->hasTaxiPath(taxiPathId)) {
if (owner_.transportManager_->assignTaxiPathToTransport(data.entry, taxiPathId)) {
LOG_DEBUG("MO_TRANSPORT entry=", data.entry, " assigned TaxiPathNode path ", taxiPathId);
}
} else {
LOG_DEBUG("MO_TRANSPORT entry=", data.entry, " taxiPathId=", taxiPathId,
" not found in TaxiPathNode.dbc");
}
}
}
}
void EntityController::handleGameObjectPageText(network::Packet& packet) {
if (!packet.hasRemaining(8)) return;
uint64_t guid = packet.readUInt64();
auto entity = entityManager.getEntity(guid);
if (!entity || entity->getType() != ObjectType::GAMEOBJECT) return;
auto go = std::static_pointer_cast<GameObject>(entity);
uint32_t entry = go->getEntry();
if (entry == 0) return;
auto cacheIt = gameObjectInfoCache_.find(entry);
if (cacheIt == gameObjectInfoCache_.end()) {
queryGameObjectInfo(entry, guid);
return;
}
const GameObjectQueryResponseData& info = cacheIt->second;
uint32_t pageId = 0;
// AzerothCore layout:
// type 9 (TEXT): data[0]=pageID
// type 10 (GOOBER): data[7]=pageId
if (info.type == 9) pageId = info.data[0];
else if (info.type == 10) pageId = info.data[7];
if (pageId != 0 && owner_.socket && owner_.state == WorldState::IN_WORLD) {
owner_.bookPages_.clear(); // start a fresh book for this interaction
auto req = PageTextQueryPacket::build(pageId, guid);
owner_.socket->send(req);
return;
}
if (!info.name.empty()) {
owner_.addSystemChatMessage(info.name);
}
}
void EntityController::handlePageTextQueryResponse(network::Packet& packet) {
PageTextQueryResponseData data;
if (!PageTextQueryResponseParser::parse(packet, data)) return;
if (!data.isValid()) return;
// Append page if not already collected
bool alreadyHave = false;
for (const auto& bp : owner_.bookPages_) {
if (bp.pageId == data.pageId) { alreadyHave = true; break; }
}
if (!alreadyHave) {
owner_.bookPages_.push_back({data.pageId, data.text});
}
// Follow the chain: if there's a next page we haven't fetched yet, request it
if (data.nextPageId != 0) {
bool nextHave = false;
for (const auto& bp : owner_.bookPages_) {
if (bp.pageId == data.nextPageId) { nextHave = true; break; }
}
if (!nextHave && owner_.socket && owner_.state == WorldState::IN_WORLD) {
auto req = PageTextQueryPacket::build(data.nextPageId, owner_.playerGuid);
owner_.socket->send(req);
}
}
LOG_DEBUG("handlePageTextQueryResponse: pageId=", data.pageId,
" nextPage=", data.nextPageId,
" totalPages=", owner_.bookPages_.size());
}
} // namespace game
} // namespace wowee