Three identical copies (game_handler.cpp, spell_handler.cpp,
quest_handler.cpp) plus two forward declarations (inventory_handler.cpp,
social_handler.cpp) replaced with a single inline definition in
game_utils.hpp. All affected files already include this header, so
quality color table changes now propagate from one source of truth.
The for-loop over {SMSG_LOOT_CLEAR_MONEY} was missing its closing brace,
so SMSG_READ_ITEM_OK and SMSG_READ_ITEM_FAILED registrations were inside
the loop body. Works by accident (single iteration) but fragile and
misleading — future additions to the loop would re-register book handlers.
Two fixes for item name resolution:
1. Clear entry from pendingItemQueries_ even when response parsing fails.
Previously a malformed response left the entry stuck in pending forever,
blocking all retries so the UI permanently showed "Item 12345".
2. Add 5-second periodic cleanup of pendingItemQueries_ so lost/dropped
responses don't permanently block item info resolution.
The handler read an extra uint8 (bag) after bagSlot, shifting all
subsequent fields by 1 byte. This caused count to straddle the count
and countInInventory fields — e.g. count=1 read as 0x03000000 (50M).
Also removes cast bar diagnostic overlay and demotes debug logs.
The cast bar window used ImGuiCond_FirstUseEver for positioning, so
ImGui's .ini state restored a stale off-screen position from a prior
session. Switch to ImGuiCond_Always and add NoSavedSettings flag so
the bar always renders centered near the bottom of the screen.
Also demotes remaining diagnostic logs to LOG_DEBUG.
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.
WotLK trade packet format was wrong in multiple ways:
- whichPlayer was read as uint8, actually uint32
- Missing tradeId field (we read tradeId as tradeCount)
- Per-slot size was 52 bytes, actually 64 (missing suffixFactor,
randomPropertyId, lockId = 12 bytes)
- tradeCount is 8 (7 trade + 1 "will not be traded"), not capped at 7
Verified: header(4+4=8) + 8×(1+64=65) + gold(4) = 532 bytes matches
the observed packet size exactly.
Note: Classic trade format differs and will need its own parser.
SMSG_TRADE_STATUS(COMPLETE) and SMSG_TRADE_STATUS_EXTENDED arrive in the
same packet batch. COMPLETE was calling resetTradeState() which cleared
all trade slots and gold BEFORE EXTENDED could write the final data.
The trade window showed "7c" (garbage gold) because the gold field read
from the wrong offset (slot size was also wrong: 60→52 bytes).
Now COMPLETE just sets status to None without full reset, preserving
trade state for EXTENDED to populate. The TRADE_CLOSED addon event
still fires correctly.
Equipment: the first emitOtherPlayerEquipment call fired before any item
queries returned, sending all-zero displayIds that stripped players naked.
Now skips the callback when resolved=0 (waiting for queries). Equipment
only applies once at least one item resolves, preventing the naked flash.
BG announcer: broadened filter to match ALL chat types (not just SYSTEM),
and added more patterns: "BGAnnouncer", "[H: N, A: N]" with spaces.
Also added diagnostic logging in setOnlinePlayerEquipment to trace
displayId counts reaching the renderer.
Domain handlers were setting `owner_.gossipWindowOpen = false` directly
on GameHandler's stale member, but isGossipWindowOpen() delegates to
QuestHandler's copy. The gossip window stayed open because the
delegating getter never saw the close.
Fix: use owner_.closeGossip() / owner_.closeVendor() which properly
delegate to QuestHandler/InventoryHandler to close the canonical state.
Affected: InventoryHandler (3 sites: mail, trainer, bank opening),
MovementHandler (1 site: taxi opening), QuestHandler (2 sites: gossip
opening closes vendor).
Stormwind players stand on WMO floors ~95m above terrain. The previous
check only tested if terrain existed at the spawn XY (it did — far below).
Now checks WMO floor first, then terrain, requiring the ground to be within
15 units of spawn Z. Falls back to tile count after 10s.
Also adds diagnostic logging for useItemBySlot (hearthstone debug).
Stride 4 was wrong — the raw dump shows entries at 284, 288, 292 which
are slots 0, 2, 4 with stride 2 (slot 1=NECK is zero because necks are
invisible). Stride 2 with base 284 correctly maps 19 equipment slots.
Added WARNING-level log when item query responses trigger equipment
re-emit for other players, to confirm the re-emit chain works.
The falling-through-world issue is likely terrain chunks not loading
fast enough — the terrain streaming stalls are still present.
RAW FIELDS dump shows equipment entries at indices 284, 288, 292, 296, 300
— stride 4, not 2. Each visible item slot occupies 4 fields (entry +
enchant + 2 padding), not 2 as previously assumed.
Field dump evidence:
[284]=3817(Reinforced Buckler) [288]=3808(Double Mail Boots)
[292]=3252 [296]=3823 [300]=3845 [312]=3825 [314]=3827
With stride 2, slots 0-18 read indices 284,286,288,290... which interleaves
entries with enchant/padding values, producing mostly zeros for equipment.
With stride 4, slots correctly map to entry-only fields.
PLAYER_VISIBLE_ITEM_1_ENTRYID for WotLK 3.3.5a is at UNIT_END(148) + 260
= field index 408 with stride 2. The previous default of 284 (UNIT_END+136)
was in the quest log field range, causing item IDs like "Lesser Invisibility
Potion" and "Deathstalker Report" to be read as equipment entries.
This was the root cause of other players appearing naked — item queries
returned valid responses but for the WRONG items (quest log entries instead
of equipment), so displayInfoIds were consumable/quest item appearances.
The heuristic auto-detection still overrides for Classic/TBC (different
stride per expansion), so this only affects the WotLK default before
detection runs.
Also filter addon whispers (GearScore GS_*, DBM, oRA, BigWigs, tab-prefixed)
from chat display — these are invisible in the real WoW client.
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:
- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module
Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.
game_handler.cpp reduced from ~10,188 to ~9,432 lines.
Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.