elapsedTime_ was float (32-bit, ~7 significant digits). At 16384
seconds the float can only represent integers, so elapsedTime_*1000
jumps in 1-second steps — ships and elevators visibly jerk. Changed to
double (53-bit mantissa) which maintains sub-millisecond precision for
~285 million years. Also changed lastServerUpdate to double to match.
vmaCreateBuffer return value was silently discarded in both loadTerrain
and loadTerrainIncremental. If allocation failed (OOM/fragmentation),
the chunk proceeded with a VK_NULL_HANDLE UBO, causing the GPU to read
from an invalid descriptor on the next draw call. Now checks the return
value and skips the chunk on failure.
The doodad set name read used raw memcpy(20 bytes) bypassing the safe
read<T> template that returns {} on OOB. A truncated WMO file would
read past the vector's storage. Added bounds check before the memcpy.
ARGB8888 decompression read pixelCount*4 bytes from mipData without
checking that mipSize was large enough — a truncated BLP caused heap
OOB reads. Also, 'int pixelCount = width * height' overflowed for
large dimensions (signed int UB). Now validates dimensions <= 4096,
uses uint32_t arithmetic, and checks mipSize >= required for ARGB8888.
std::toupper(int) and std::tolower(int) have undefined behavior when
passed a negative value. These sites passed raw signed char without
casting to unsigned char first, unlike the rest of the codebase which
already uses the correct pattern. Affects auth (account names), world
packets, and mount sound path matching.
1. MCNK sub-chunk bounds checks didn't account for the 8-byte header
skip, so parseMCVT/parseMCNR could read up to 8 bytes past the
validated buffer when sub-chunk headers are present (the common case).
2. parseMTEX/parseMMDX/parseMWMO used unbounded strlen on raw chunk
data. A truncated file without a null terminator would read past the
chunk boundary. Replaced with strnlen bounded by remaining size.
Also removes dead debug code: empty magic buffer copy, cathedral WMO
search, and Stormwind placement dump (which also had ::toupper UB).
pendingPlayerSpawns_, deferredEquipmentQueue_, and
pendingGameObjectSpawns_ are consumed from the front via erase(begin()),
which is O(n) on vector (shifts all elements). With many spawns queued
(entering a city), this made the processing loop O(n²). deque supports
O(1) front erasure. pendingCreatureSpawns_ already used deque.
readArray was called inside the loop on every iteration, re-parsing the
entire flat key array via memcpy. For a model with 200 sequences and
10k keys this produced ~24MB of redundant copying. Now reads once before
the loop (matching how allTimestamps was already handled).
WotLK format is min(4)+max(4)+result(4)+guid(8)=20 bytes. The parser
read guid(8) first (treating min|max as a uint64), then targetGuid(8)
(non-existent field), then the actual values at wrong offsets. Every
/roll message showed garbled numbers and a bogus roller identity.
Also adds a hasRemaining guard for the 64 bytes of damage/armor/resist
fields in the item query parser — previously read past end with silent
zero-fill on truncated packets.
The comment would lead a maintainer to "fix" the working code to read
4-byte RGBA instead of 3-float C3Vector. Updated to match the actual
M2 particle FBlock color format (3 floats, values 0-255, per WoWDev).
std::tolower(int) has undefined behavior when passed a negative value,
which signed char produces for bytes > 127. The rest of the codebase
correctly casts to unsigned char first; these 3 sites were missed.
The golden tint rect was drawn before rendering with a hardcoded single-
line height. Multi-line wrapped messages only had the first line
highlighted. Now drawn after EndGroup() using GetItemRectMin/Max so the
rect covers all wrapped lines.
Also fixes std::tolower(char) UB at two call sites — negative char
values (extended ASCII) are undefined behavior without unsigned cast.
Both operands are size_t (unsigned), so if readPos > data.size() the
subtraction wrapped to ~0 instead of returning 0. This could happen
via setReadPos() which has no bounds check. Downstream hasRemaining()
was already safe but getRemainingSize() callers (e.g. hasFullPackedGuid)
would see billions of bytes available.
A single send() that returned fewer bytes than requested was logged but
not retried, leaving the server with a truncated packet. This causes an
irreversible TCP framing desync (next header lands mid-payload) that
manifests as a disconnect under network pressure. Added a retry loop
that handles EWOULDBLOCK with a brief yield.
Also rejects payloads > 64KB instead of silently truncating the 16-bit
CMSG size field, which would have written a wrong header while still
appending all bytes.
The two emit calls were indented 12 spaces (suggesting a nested block)
instead of 8 (matching the enclosing if). Same class of maintenance
trap as the PLAYER_ALIVE/PLAYER_UNGHOST fix in b3abf04d.
1. Contradictory condition (!numStrings && numStrings >= 1) was always
false, so unknown guild event messages never included the server's
context string. Fixed to just numStrings >= 1.
2. leaveParty() only sent the packet without clearing partyData or
firing addon events, so /leave left party frames visible until the
server pushed an update. Now delegates to leaveGroup() which handles
both the packet and local state cleanup.
The cast falsely suggests reasonType is unused, but it's read on lines
3699-3702 for AFK/vote-kick differentiation. Same class of issue as
the (void)isPlayerTarget fix in commit 6731e584.
handleFriendStatus inserted into friendsCache with an empty playerName
when the name query hadn't resolved yet, creating a phantom "" entry.
Now guards with !playerName.empty().
removeIgnore erased from ignoreCache immediately without waiting for
server confirmation, desyncing the cache if the server rejected. Now
only clears the GUID set and lets the next SMSG_IGNORE_LIST rebuild
the cache, consistent with how removeFriend works.
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 per-tick GPU upload budget check ran before consuming async futures,
so after 1 upload ALL remaining ready results were deferred — including
permanent failures and cache hits that need zero GPU work. Moved the
budget gate after failure/cache-hit processing so only actual uploads
count. Re-queues over-budget results as pending spawns for next frame.
Callbacks and addons querying the current target during this event saw
the old (stale) target instead of null. setTarget correctly updates the
GUID before firing — clearTarget now does the same.
The spellbook tab dirty check used a function-local static, meaning
switching to a character with the same spell count would skip the
rebuild and return the previous character's tabs. Changed to an
instance member so each SpellHandler tracks its own count.
Freshly spawned entities have maxHealth=0 before fields populate.
0/0 produces NaN which propagates through all geometry calculations
for that nameplate frame. Guard with a maxHealth>0 check.
All five force-ACK handlers (speed, root, flag, collision-height,
knockback) repeated the same ~25-line GUID+counter+movementInfo+coord-
conversion+send sequence. Extracted into buildForceAck() which returns
a ready-to-send packet with the movement payload already written.
This also fixes a transport coordinate conversion bug: the collision-
height handler was the only one that omitted the ONTRANSPORT check,
causing position desync when riding boats/zeppelins. buildForceAck
handles transport coords uniformly for all callers.
Net -80 lines.
Loop iterated 20 hair geoset lookups for Human Male but the if-body
was empty — the LOG statement that was presumably there was removed
but the loop skeleton was left behind.
A function for taxi/movement cleanup was resetting 10 death-related
fields (playerDead_, releasedSpirit_, resurrectPending_, etc.), which
could cancel a pending resurrection or mark a dead player as alive
when called during taxi dismount. Death state is owned by
entity_controller and resurrect packet handlers, not movement cleanup.
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.
The character-list level update loop used 'return' instead of 'break',
exiting the handler lambda before the level-up chat message, sound
effect, callback, and PLAYER_LEVEL_UP event could fire. Since the
player GUID is always in the character list, the notification code
was effectively dead — players never saw "You have reached level N!".
The variable is used earlier in the function for hostile attacker
tracking, so the (void) cast falsely suggests it was unused. Leftover
from a prior refactor.
Icon==6 and text=="GOSSIP_OPTION_BANKER" both sent BANKER_ACTIVATE
independently. Banking NPCs match both, so the packet was sent twice —
some servers toggle the bank window open then closed. Added sentBanker
guard so only one packet is sent.
Also extracts classifyGossipQuests() from two identical 30-line blocks
in handleGossipMessage and handleQuestgiverQuestList. The icon→status
mapping (5/6/10=completable, 3/4=incomplete, 2/7/8=available) is now
in one place with a why-comment explaining these are protocol-defined.
If the server sent a NaN or out-of-range speed, the client echoed it
back in the ACK (confirming it to the server) but then rejected it
locally. This left the server believing the client accepted the speed
while the client used the old value — a desync only fixable by relog.
Moved validation before the ACK so bad speeds are rejected outright.
selectCharacter had 30+ if(spellHandler_) guards reaching into
SpellHandler internals (knownSpells_, spellCooldowns_, playerAuras_,
etc.) to clear per-character state. Consolidated into resetAllState()
so SpellHandler owns its own reset logic and new state fields don't
require editing GameHandler.
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.
The 10s silence warning used a one-shot bool guard, but the 15s warning
used a 500ms time window — firing every frame (~30 times at 60fps).
Added rxSilence15sLogged_ guard consistent with the 10s pattern.
!packet.hasRemaining(4) + 4 + 4 evaluated as (!hasRemaining(4))+8
due to ! binding tighter than +, making the check always truthy and
breaking out of the loop after the first pet. Hunters with multiple
stabled pets would see only one in the stable master UI.
handleSpellStart and handleSpellGo duplicated the player/target/focus/
pet GUID-to-unitId mapping that already exists in guidToUnitId(). If a
new unit-id category is added (e.g. mouseover), these inline copies
would not pick it up.
SMSG_PET_GUIDS, SMSG_PET_DISMISS_SOUND, and SMSG_PET_ACTION_SOUND were
registered with the same handler as SMSG_PET_UNLEARN_CONFIRM. Their
different formats (GUID lists, sound IDs with position) were misread as
unlearn cost, potentially triggering a bogus unlearn confirmation dialog.
Also extracts resetWardenState() from 13 lines duplicated verbatim
between connect() and disconnect().
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.
The heartbeat throttle bitmask was missing SWIMMING and DESCENDING,
treating swimming/descending players as stationary and using a slower
heartbeat interval. The identical bitmask in movement_handler.cpp
already included SWIMMING — this inconsistency could cause the server
to miss position updates during swim combat.
Only ASCENDING was cleared — the DESCENDING flag was never toggled,
so outgoing movement packets during flight descent had incorrect flags.
Also clears DESCENDING on start-ascend and stop-ascend for symmetry.
Replaces static heartbeat log counter with member variable (was shared
across instances and not thread-safe) and demotes to LOG_DEBUG.
SpellHandler::updateTimers() (added in 209c2577) already ticks down
castTimeRemaining_, unitCastStates_, and spellCooldowns_. But the
GameHandler::update() loop also ticked them manually — causing casts to
complete at 2x speed and cooldowns to expire twice as fast.
Removed the duplicate tick-downs from update(). The GO interaction
completion check remains (client-timed casts need this fallback).
Also uses resetCastState() instead of manually clearing 4 fields,
adds missing castTimeTotal_ reset, and adds loadSpellNameCache()
to getSpellName/getSpellRank (every other DBC getter had it).
SMSG_ENVIRONMENTALDAMAGELOG (alias) registration at line 173 silently
overwrote the canonical SMSG_ENVIRONMENTAL_DAMAGE_LOG handler at line
108. The alias handler discarded envType (fall/lava/drowning), so the
UI couldn't differentiate environmental damage sources. Removed the
dead alias handler and its method; the canonical inline handler with
envType forwarding is now the sole registration.
Quest log had a redundant pos=0 right after initialization. Chat handler
logged every incoming/outgoing message at WARNING level, flooding the
log and obscuring genuine warnings.
taxiRecoverPending_ was unconditionally reset to false in the general
state cleanup, 39 lines before the recovery check that reads it. The
recovery block could never execute. Removed the premature clear so
mid-flight disconnect recovery can actually trigger.
The packet only contains uint8 count + count×uint64 GUIDs, but the
handler called readString() after each GUID. This consumed raw bytes of
subsequent GUIDs as a string, corrupting all entries after the first.
Now stores GUIDs in ignoreListGuids_ and resolves names asynchronously
via SMSG_NAME_QUERY_RESPONSE, matching the friends list pattern.
Also fixes unsafe static_pointer_cast in ready check (no type guard)
and removes redundant packetHasRemaining wrapper (duplicates Packet API).
All spline speed opcodes share the same PackedGuid+float format,
differing only in which member receives the value. Replaced 8 identical
lambdas (~55 lines) with a makeSplineSpeedHandler factory that captures
a member pointer, cutting duplication and making it trivial to add new
speed types.
Both the health==0 and dynFlags UNIT_DYNFLAG_DEAD paths duplicated the
same corpse-position caching and death-state logic with a subtle
asymmetry (only health path called stopAutoAttack). Extracted into
markPlayerDead() so coordinate swapping and state changes happen in one
place. stopAutoAttack remains at the health==0 call site since the
dynFlags path doesn't need it.
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.