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 = PLAYER_FIELD_INV_SLOT_HEAD(324) - 19*2
= 286. The previous value of 408 landed far past inventory slots in
string/name data, producing garbage entry IDs (ASCII fragments like
"mant", "alk ", "ryan") that the server rejected as invalid items.
Derivation: 19 visible item slots × 2 fields (entry + enchant) = 38
fields immediately before PLAYER_FIELD_INV_SLOT_HEAD at index 324.
The Classic fallback silently succeeded on WotLK data by false-positive
matching, consuming wrong bytes and producing corrupt entity data that
was silently dropped — resulting in zero other players/NPCs visible.
Now tries 4 WotLK-only variants in order:
1. Full WotLK (durationMod+durationModNext+vertAccel+effectStart+compressed)
2. Full WotLK uncompressed
3. WotLK without parabolic fields (durationMod+durationModNext+points)
4. WotLK without parabolic, compressed
This covers servers that don't unconditionally send vertAccel+effectStart
(the MEMORY.md says AzerothCore does, but other cores may not).
The previous fix (b8a9efb7) that returned false on spline failure was too
aggressive — it aborted the ENTIRE UPDATE_OBJECT packet, not just one
block. Since many entity spawns (NPCs, other players) share the same
packet, a single spline parse failure killed ALL entities in the batch.
Restored the Classic-format fallback as a last resort after WotLK format
fails. The key difference from the original bug is that WotLK is now
tried FIRST (with proper position save/restore), and Classic only fires
if WotLK fails. This prevents the false-positive match that originally
caused corruption while still handling edge-case spline formats.
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.
SMSG_GAMEOBJECT_CUSTOM_ANIM with animId=0 on a fishing node (type 17)
was triggering "A fish is on your line!" for ALL fishing bobbers in
range, including other players'. Now checks OBJECT_FIELD_CREATED_BY
(fields 6-7) matches the local player GUID before showing the message.
When both WotLK compressed and uncompressed spline point parsing fail,
the parser was silently continuing with a corrupted read position (16
bytes of WotLK spline header already consumed). This caused the update
mask to read garbage (maskBlockCount=40), corrupting the current entity
AND all remaining blocks in the same UPDATE_OBJECT packet.
Now returns false on spline failure, cleanly aborting the single block
parse and allowing the remaining blocks to be recovered (if the parser
can resync). Also logs the failing GUID and spline flags for debugging.
This fixes:
- Entities spawning with displayId=0/entry=0 (corrupted parse)
- "Unknown update type: 128" errors from reading garbage
- Falling through the ground (terrain entities lost in corrupted batch)
- Phantom "fish on your line" from fishing bobber entity parse failure
Warden module download (18756 bytes, 38 chunks of 500 bytes) stalled at
32 chunks because the per-pump packet parse budget was 16 — after two
2ms pump cycles (32 packets), the TCP receive buffer filled and the
server stopped sending. Character list never arrived.
- kDefaultMaxParsedPacketsPerUpdate: 16 → 64
- kDefaultMaxPacketCallbacksPerUpdate: 6 → 48
Also adds WARNING-level diagnostic logs for auth pipeline packets and
Warden module download progress (previously DEBUG-only, invisible in
production logs).
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/.
CONTRIBUTING.md: code style, PR process, architecture pointers, packet
handler pattern, key files for new contributors.
CHANGELOG.md: grouped changes since v1.8.1-preview into Performance,
Bug Fixes, Features, Security, and Code Quality sections.
Chat parser: use stack-allocated std::array<char, 256> for typical chat
messages instead of heap-allocated std::string. Only falls back to heap
for messages > 256 bytes. Reduces allocator pressure on high-frequency
chat packet handling.
Entity storage: std::map<uint64_t, shared_ptr<Entity>> → unordered_map for
O(1) entity lookups instead of O(log n). No code depends on GUID ordering.
Player skills: std::map<uint32_t, PlayerSkill> → unordered_map.
DBC ID cache: std::map<uint32_t, uint32_t> → unordered_map.
Warden: apiHandlers_ and allocations_ → unordered_map (freeBlocks_ kept
as std::map since its coalescing logic requires ordered iteration).
Contacts: handleFriendStatus() did 3 separate O(n) find_if scans per
packet. Consolidated to single find_if with iterator reuse. O(3n) → O(n).
M2 renderer: move 3 per-frame local containers to member variables:
- particleGroups_ (unordered_map): reuse bucket structure across frames
- ribbonDraws_ (vector): reuse draw call buffer
- shadowTexSetCache_ (unordered_map): reuse descriptor cache
Eliminates ~3 heap allocations per frame in particle/ribbon/shadow passes.
UI polish:
- Nameplate hover tooltip showing level, class (players), guild name
- Bag window titles show slot counts: "Backpack (12/16)"
Player report: CMSG_COMPLAIN packet builder and reportPlayer() method.
"Report Player" option in target frame right-click menu for other players.
Server response handler (SMSG_COMPLAIN_RESULT) was already implemented.
NPC and other-player melee swing callback was hardcoded to animation 16
(unarmed attack). Now tries 17 (1H weapon), 18 (2H weapon) first with
hasAnimation() check, falling back to 16 if neither exists on the model.
M2 renderer: skip bone matrix computation for instances beyond 150 units
(LOD 3 threshold). These models use minimal static geometry with no visible
skeletal animation. Last-computed bone matrices are retained for GPU upload.
Removes unnecessary float matrix operations for hundreds of distant NPCs
in crowded zones.
Water renderer: add per-surface AABB frustum culling before draw calls.
Computes tight AABB from surface corners and height range, tests against
camera frustum. Skips descriptor binding and vkCmdDrawIndexed for surfaces
outside the view. Handles both ADT and WMO water (rotated step vectors).
Equipment: removed the visibleItemLayoutVerified_ gate from
updateOtherPlayerVisibleItems(). The default WotLK field layout (base=284,
stride=2) is correct and should be used immediately. The verification
heuristic was silently blocking ALL other-player equipment rendering by
queuing for auto-inspect (which doesn't return items in WotLK anyway).
Follow: auto-follow now uses run speed (autoRunning) instead of walk speed.
Also uses squared distance for the distance checks.
Commands: /quit, /exit aliases for /logout; /difficulty normal/heroic/25/25heroic
sends CMSG_CHANGEPLAYER_DIFFICULTY.
Inspect: CMSG_INSPECT was writing full uint64 GUID instead of packed GUID.
Server silently rejected the malformed packet. Fixed both InspectPacket and
QueryInspectAchievementsPacket to use writePackedGuid().
Follow: was a no-op (only stored GUID). Added client-side auto-follow system:
camera controller walks toward followed entity, faces target, cancels on
WASD/mouse input, stops within 3 units, cancels at 40+ units distance.
Party commands:
- /lootmethod (ffa/roundrobin/master/group/nbg) sends CMSG_LOOT_METHOD
- /lootthreshold (0-5 or quality name) sets minimum loot quality
- /raidconvert converts party to raid (leader only)
Equipment diagnostic logging still active for debugging naked players.
Target frame: add Follow, Clear Target, and Set Raid Mark submenu to the
right-click context menu (Inspect, Trade, Duel were already present).
Equipment diagnostics: add LOG_INFO traces to updateOtherPlayerVisibleItems()
and emitOtherPlayerEquipment() to debug why other players appear naked.
Logs the visible item entry IDs received from the server and the resolved
displayIds from itemInfoCache. Check the log for "emitOtherPlayerEquipment"
to see if entries arrive as zeros (server not sending fields) or if
displayIds are zero (item templates not cached yet).
Shoulder pieces are M2 model attachments (like helmets), not body geosets.
Load left shoulder at attachment point 5, right shoulder at point 6.
Models resolved from ItemDisplayInfo.dbc LeftModel/RightModel fields,
with race/gender suffix variants tried first. Applied to both online
player and NPC equipment paths.
Other players previously appeared partially naked — only chest, legs, feet,
hands, cape, and tabard rendered. Now renders full equipment:
- Helmet M2 model: loads from ItemDisplayInfo.dbc with race/gender suffix,
attaches at head bone (point 0/11), hides hair geoset under helm
- Weapons: mainhand (attachment 1) and offhand (attachment 2) M2 models
loaded from ItemDisplayInfo, with Weapon/Shield path fallback
- Wrist/bracer geoset (group 8): applies when no chest sleeve overrides
- Belt/waist geoset (group 18): reads GeosetGroup1 from ItemDisplayInfo
- Shoulder M2 attachments deferred (separate bone attachment system)
Also applied same wrist/waist geosets to NPC and character preview paths.
Minimap: batch 9 individual vkUpdateDescriptorSets into single call.
Mail: change money/COD fields from uint32 to uint64 in CMSG_SEND_MAIL and
SMSG_MAIL_LIST_RESULT for WotLK 3.3.5a. Classic keeps uint32 on the wire.
Fixes money truncation and packet misalignment causing mail failures.
Other-player capes: add cape texture loading to setOnlinePlayerEquipment().
The cape geoset was enabled but no texture was loaded, leaving capes blank.
Now mirrors the local-player path: looks up ItemDisplayInfo.dbc, finds cape
texture candidates, applies via setGroupTextureOverride/setTextureSlotOverride.
Zone toasts: suppress duplicate zone toast when the zone text overlay is
already showing the same zone name. Fixes double "Entering: Stormwind City".
Network: enable TCP_NODELAY on both auth and world sockets after connect(),
disabling Nagle's algorithm to eliminate up to 200ms buffering delay on
small packets (movement, spell casts, chat).
Rendering: track material and bone descriptor sets in M2 renderer to skip
redundant vkCmdBindDescriptorSets calls between batches sharing same textures.
- Replace count()+operator[] double lookups with find() or try_emplace()
in gameObjectInstances_, playerTextureSlotsByModelId_, onlinePlayerAppearance_
- Add Entity::isUnit() helper; replace 5 dynamic_cast<Unit*> in per-frame
UI rendering (nameplates, combat text, pet frame) with isUnit()+static_cast
- Add constexpr kInv255 reciprocal for per-pixel normal map generation loops
in character_renderer and wmo_renderer
Spline parsing: remove Classic format fallback from the WotLK parser. The
PacketParsers hierarchy already dispatches to expansion-specific parsers
(Classic/TBC/WotLK/Turtle), so the WotLK parseMovementBlock should only
attempt WotLK spline format. The Classic fallback could false-positive when
durationMod bytes resembled a valid point count, corrupting downstream parsing.
Preload DBC caches: call loadSpellNameCache() and 5 other lazy DBC caches
during handleLoginVerifyWorld() on initial world entry. This moves the ~170ms
Spell.csv load from the first SMSG_SPELL_GO handler to the loading screen,
eliminating the mid-gameplay stall.
WMO portal culling: move per-instance portalVisibleGroups vector and
portalVisibleGroupSet to reusable member variables, eliminating heap
allocations per WMO instance per frame.
Spline auto-detection: try WotLK format before Classic to prevent false-positive
matches where durationMod float bytes resemble a valid Classic pointCount. This
caused the movement block to consume wrong byte count, corrupting the update mask
read (maskBlockCount=57/129/203 instead of ~5) and silently dropping NPC spawns.
Terrain latency: bound WMO liquid group loading to 4 groups per advanceFinalization
call. Large WMOs (e.g., Stormwind canals with 40+ liquid groups) previously loaded
all groups in one unbounded loop, blowing past the 8ms frame budget and causing
stalls up to 1300ms. Now yields back to processReadyTiles() after 4 groups so the
time budget check can break out.
- Hoist DBC field index lookups before loops in game_handler (7 DBC iteration loops)
- Cache getSkybox()/getPosition() calls instead of redundant per-frame queries
- Merge textureHasAlphaByPtr_ + textureColorKeyBlackByPtr_ into single map
- Add constexpr for DEG_TO_RAD, reciprocal constants, physics delta
- Add reserve() for WMO/M2 collision grid queries and portal BFS
- Frustum plane normalize: inversesqrt instead of length+divide
- M2 particle emission: inversesqrt for direction normalization
- Parse creature display IDs from query response
- UI: show spell names/IDs as fallback instead of "Unknown"
Squared distance optimizations across 30 files:
- Convert glm::length() comparisons to glm::dot() (no sqrt)
- Use glm::inversesqrt() for check-then-normalize patterns (1 rsqrt vs 2 sqrt)
- Defer sqrt to after early-out checks in collision/movement code
- Hottest paths: camera_controller (21), weather particles, WMO collision,
transport movement, creature interpolation, nameplate culling
Container and algorithm improvements:
- std::map<string> → std::unordered_map for asset/DBC/MPQ/warden caches
- std::mutex → std::shared_mutex for asset_manager and mpq_manager caches
- std::sort → std::partial_sort in lighting_manager (top-2 of N volumes)
- Double-lookup find()+operator[] → insert_or_assign in game_handler
- Add reserve() for per-frame vectors: weather, swim_effects, WMO/M2 collision
Threading and synchronization:
- Replace 1ms busy-wait polling with condition_variable in character_renderer
- Move timestamp capture before mutex in logger
- Use memory_order_acquire/release for normal map completion signaling
API additions:
- DBC getStringView()/getStringViewByOffset() for zero-copy string access
- Parse creature display IDs from SMSG_CREATURE_QUERY_SINGLE_RESPONSE
Add [[nodiscard]] to VkShaderModule::loadFromFile, Shader::loadFromFile/
loadFromSource, AssetManifest::load, DbcLoader::load — all return bool
indicating success/failure that callers should check.
Suppress with (void) at 17 call sites where validity is checked via
isValid() after loading rather than the return value (m2_renderer
recreatePipelines, swim_effects recreatePipelines).
loadTexture() is called from terrain worker threads, but the static
unordered_set dedup caches for missing-texture and decode-failure
warnings had no synchronization. Add std::mutex guards around both
log-dedup blocks to prevent data races.
Add kCastGreen (interruptible cast bar, 5 uses) and kQueueGreen
(queue status / talent met, 7 uses across game_screen + talent_screen).
Remove commented-out renderQuestMarkers call (replaced by 3D billboards).
terrain_manager: replace bare 4096/2048/0x80/0x7F with named constants
ALPHA_MAP_SIZE, ALPHA_MAP_PACKED, ALPHA_FILL_FLAG, ALPHA_COUNT_MASK
— documents the WoW alpha map RLE format.
character_renderer: replace bare 256/512 texture sizes with
kBaseTexSize/kUpscaleTexSize for NPC skin upscaling logic.
The findMemType/findMemoryType helper in auth_screen, loading_screen,
and vk_context returned 0 on failure — a valid memory type index.
Changed to return UINT32_MAX and log an error, so vkAllocateMemory
receives an invalid index and fails cleanly rather than silently
using the wrong memory type.
Add [[nodiscard]] to VkBuffer::uploadToGPU/createMapped and
VkContext::initialize/recreateSwapchain so callers that ignore
failure are flagged at compile time. Suppress with (void) cast at
3 call sites where failure is non-actionable (resize best-effort).
game_screen: fsrScales, fsrScaleFactors, kTotemInfo, kRaidMarks,
kTimerInfo, kNPMarks, kCellMarks, kPartyMarks, kMMMarks, kCatOrder
keybinding_manager: actionMap
All static const arrays in UI files are now constexpr where possible.
- Move itemKeys/spellKeys/thrKeys to shared kItemSetItemKeys/
kItemSetSpellKeys/kItemSetThresholdKeys in ui_colors.hpp, removing
5 identical local definitions across game_screen and inventory_screen
- Widen totem timer snprintf buffer from 8 to 16 bytes (defensive)
- Promote kStatTooltips to constexpr
Deduplicate class/race bitmask arrays (3 copies each → 1 shared) and
socket type definitions (3 copies → 1 shared). Eliminates ~80 lines of
repeated struct definitions across game_screen.cpp and inventory_screen.cpp.
The uncompressed spline skip loop used `pointCount - 1` in its bound
without guarding pointCount > 1. While pointCount==0 is already handled
by an early return, pointCount==1 would correctly iterate 0 times, but
the explicit guard makes the intent clearer and prevents future issues
if the early return is ever removed.
- Move kDispelNames to file-scope constexpr, removing 2 duplicate local
definitions in raid/party frame rendering
- Promote kTotemColors and kReactDimColors from static const to constexpr
- Replace std::to_string + string concat for ImGui widget IDs with
snprintf into stack buffers (avoids heap allocations in render loops)