The decomposition PRs moved mail state to InventoryHandler but the GO
interaction code still set stale GameHandler fields. Add openMailbox()
on InventoryHandler and forward from GameHandler so the correct
mailboxGuid_/mailboxOpen_ are set and refreshMailList() works.
Calculate repair costs client-side using DurabilityCosts.dbc and
DurabilityQuality.dbc. Block repair when player can't afford it and
only apply optimistic durability/gold updates when cost is verified.
Show repair cost next to the Repair All button in the vendor window.
handleValuesUpdate silently dropped VALUES updates for item GUIDs not in
entityManager, causing repair-all durability changes to be lost. Fall
through to updateItemOnValuesUpdate for items tracked in onlineItems_.
auctionSellItem now resolves the item GUID internally via
backpackSlotGuids_ with resolveOnlineItemGuid fallback, matching the
pattern used by vendor sell and item use. Previously the UI passed
the GUID directly from getBackpackItemGuid() with no fallback, so
items with unset slot GUIDs silently failed to list.
Also gates CMSG_AUCTION_SELL_ITEM format by expansion: Classic/TBC
omits the itemCount and stackCount fields that WotLK requires.
ListInventoryParser::parse() was resetting the entire ListInventoryData
struct, wiping the canRepair flag set by the gossip handler before the
server response arrived. Preserve it across the parse.
Also detect repair capability from UNIT_NPC_FLAG_REPAIR (0x1000) on the
vendor NPC entity, so direct vendors without gossip menus also show the
repair button.
- Add Inventory::FIRST_BAG_EQUIP_SLOT = 19 constant with why-comment
explaining WoW equip slot layout (bags occupy slots 19-22)
- Replace all 19 occurrences of magic number 19 in bag slot calculations
across inventory_handler, spell_handler, inventory, and game_handler
- Add UNIT_FIELD_FLAGS / UNIT_FLAG_PVP comment in combat_handler
- Add why-comment on network packet budget constants (prevent server
data bursts from starving the render loop)
- Replace all 11 occurrences of magic number 23 in backpack slot
calculations with Inventory::NUM_EQUIP_SLOTS across inventory_handler,
spell_handler, and inventory.cpp
- Add why-comment to NUM_EQUIP_SLOTS explaining WoW slot layout
(equipment 0-22, backpack starts at 23 in bag 0xFF)
- Add why-comment on 0x80000000 bit mask in item query response
(high bit flags negative/missing entry response)
- Replace manual channel membership loops with std::find in
chat_handler.cpp (YOU_JOINED and PLAYER_ALREADY_MEMBER cases)
- Add why-comment on PLAYER_ALREADY_MEMBER reconnect edge case
Introduce `GameServices` struct — an explicit dependency bundle that
`Application` populates and passes to `GameHandler` at construction time.
Eliminates all 47 hidden `Application::getInstance()` calls in
`src/game/*.cpp`, completing SOLID-D (dependency-inversion) cleanup.
Changes:
- New `include/game/game_services.hpp` — `struct GameServices` carrying
pointers to `Renderer`, `AssetManager`, `ExpansionRegistry`, and two
taxi-mount display IDs
- `GameHandler(GameServices&)` replaces default constructor; exposes
`services() const` accessor for domain handlers
- `Application` holds `game::GameServices gameServices_`; populates it
after all subsystems are created, then constructs `GameHandler`
(fixes latent init-order bug: `GameHandler` was previously created
before `AssetManager` / `ExpansionRegistry`)
- `game_handler.cpp`: duplicate `isActiveExpansion` / `isClassicLikeExpansion` /
`isPreWotlk` anonymous-namespace helpers removed; `game_utils.hpp`
included instead
- All domain handlers (`InventoryHandler`, `SpellHandler`, `MovementHandler`,
`CombatHandler`, `QuestHandler`, `SocialHandler`, `WardenHandler`) replace
`Application::getInstance().getXxx()` with `owner_.services().xxx`
Adds [GO-DIAG] WARNING-level logs at:
- Right-click dispatch (raypick hit / re-interact with target)
- interactWithGameObject entry + all BLOCKED paths
- SMSG_SPELL_GO (wasInTimedCast, lastGoGuid, pendingGoGuid state)
- SMSG_LOOT_RESPONSE (items, gold, guid)
- Raypick candidate GO positions (entity pos + hit center + radius)
These logs will pinpoint exactly where the interaction fails:
- No GO-DIAG lines = GOs not in entity manager / not visible
- Raypick GO pos=(0,0,0) = GO position not set from update block
- BLOCKED = guard condition preventing interaction
- SPELL_GO wasInTimedCast=false = timer race (already fixed)
The Packet::skipAll() method was introduced to replace the verbose
setReadPos(getSize()) pattern. 186 instances were migrated earlier,
but 20 survived in domain handler files created after the migration.
Also removes a redundant single-element for-loop wrapper around
SMSG_LOOT_CLEAR_MONEY registration.
All domain handler files used 'packet.getSize() - packet.getReadPos()'
which underflows to ~2^64 when readPos exceeds size (documented in
commit ed63b029). The game_handler.cpp and packet_parsers were migrated
to hasRemaining(N) in an earlier cleanup, but the domain handlers were
created after that migration by the PR #23 split, copying the old
unsafe patterns back in. Now uses hasRemaining(N) for comparisons and
getRemainingSize() for assignments across all 7 handler files.
The handler treated the second uint32 (auctionId) as itemEntry. The
real itemEntry is at byte 24 after auctionHouseId(4)+auctionId(4)+
bidderGuid(8)+bidAmount(4)+outbidAmount(4). Outbid chat messages always
referenced the wrong item.
The same 25-line block copying ~20 fields from itemInfoCache_ into
ItemDef was duplicated for equipment, backpack, keyring, and bag slots.
Extracted into buildItemDef() so new fields only need adding once.
Net -100 lines.
The auto-refresh after successful bid/buyout was gated on
lastAuctionSearch_.name.length() > 0, so a browse-all search (empty
name) would never refresh. Replaced with a hasAuctionSearch_ flag
that's set on any search regardless of the name filter.
reinterpret_cast<float*> on raw packet bytes is undefined behavior per
the C++ strict aliasing rule — compilers can optimize assuming uint8_t
and float never alias. Replaced with packet.readFloat() which uses
memcpy internally. Also switched to hasRemaining() for consistency.
Both buyBackItem() and the retry path in handleBuyFailed constructed
packets with a raw opcode constant instead of using the expansion-aware
BuybackItemPacket::build(). This would silently break if any expansion's
CMSG_BUYBACK_ITEM wire mapping diverges from 0x290.
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/.