Vanilla 1.12 SMSG_QUESTGIVER_QUEST_DETAILS includes rewardXp (uint32)
after rewardMoney, same as WotLK. Without this read the XP reward was
always 0 in the quest accept dialog for Classic.
The verbose diagnostic logs added in 16cdde8 for Classic equipment debugging
are no longer needed now that the CSV string-detection bug is fixed. Remove
them to eliminate log spam on every character screen open.
Also replace the hardcoded `14 + region` texture field lookup with the same
DBC-layout-aware array pattern used in game_screen.cpp::updateCharacterTextures,
so texture field indices are correctly resolved per expansion.
The string-column auto-detector in both tools had two gaps that caused small
integer fields (RaceID=1, SexID=0/1, BaseSection, ColorIndex) to be falsely
classified as string columns, corrupting the generated CSVs:
1. No boundary check: a value of N was accepted as a valid string offset even
when N landed inside a longer string (e.g. offset 3 inside "Character\...").
Fix: precompute valid string-start boundaries (offset 0 plus every position
immediately after a null byte); reject offsets that are not boundaries.
2. No diversity check: a column whose only non-zero value is 1 would pass the
boundary test because offset 1 is always a valid boundary (it follows the
mandatory null at offset 0). Fix: require at least 2 distinct non-empty
string values before marking a column as a string column. Columns like
SexID (all values are 0 or 1, resolving to "" and the same path fragment)
are integer fields, not string fields.
Both dbc_to_csv and asset_extract now produce correct column metadata,
e.g. CharSections.dbc yields "strings=6,7,8" instead of "strings=0,1,...,9".
Five movement control response handlers (speed change, move-root, move-flag
change, knock-back, teleport) had guards of the form !isClassicLikeExpansion()
or isClassicLikeExpansion() that prevented ACKs from ever being sent on
Classic/Turtle. Each handler already contained correct legacyGuidAck logic
(full uint64 for Classic/TBC, packed GUID for WotLK) that was unreachable
due to the outer guard.
Classic servers (CMaNGOS/VMaNGOS/ChromieCraft) expect all of these ACKs.
Without them the server stalls the player's speed update, keeps root state
desynced, or generates movement hacks. Fix by removing the erroneous
expansion guard and relying on the existing legacyGuidAck path.
Affected: handleForceSpeedChange, handleForceMoveRootState,
handleForceMoveFlagChange, handleMoveKnockBack, handleTeleport.
dbc_to_csv: The string-column auto-detector would mark integer fields (e.g.
RaceID=1, SexID=0, BaseSection=0-4) as string columns whenever their small
values were valid string-block offsets that happened to land inside longer
strings. Fix by requiring that an offset point to a string *boundary* (offset
0 or immediately after a null byte) rather than any valid position — this
eliminates false positives from integer fields whose values accidentally alias
path substrings. Affected CSVs (CharSections, ItemDisplayInfo for Classic/TBC)
can now be regenerated correctly.
game_handler: clearDBCCache() is already called by application.cpp before
resetDbcCaches(), but also add it inside resetDbcCaches() as a defensive
measure so that future callers of resetDbcCaches() alone also flush stale
expansion-specific DBC data (CharSections, ItemDisplayInfo, etc.).
Vanilla 1.12 SMSG_QUESTGIVER_QUEST_DETAILS includes an emote section
between suggestedPlayers and the choice/reward item lists:
activateAccept(u8) + suggestedPlayers(u32) +
emoteCount(u32) + [delay(u32) + type(u32)] × emoteCount +
choiceCount(u32) + choices + rewardCount(u32) + rewards + money(u32)
The parser was skipping the emote section, causing the emote count to
be misread as the choice item count. Quests with emotes would show
zero choice items and shifted/missing reward and money data.
Log each equipment item's displayModel, inventoryType, and DBC lookup result
to help identify why Classic character equipment does not render correctly.
Also log ItemDisplayInfo.dbc field count, found texture names per region,
and missing texture paths so the exact failure point is visible in logs.
Classic 1.12 and TBC 2.4.3 don't include questFlags(u32) + isRepeatable(u8)
before each quest title in SMSG_QUESTGIVER_QUEST_LIST. WotLK 3.3.5a added
those 5 bytes. The previous code read them speculatively for all expansions
and only rewound on empty title, which failed for any non-empty title.
Also fix questCount always reading as uint8 (all WoW versions use u8 here).
The old u32/u8 heuristic could misread 4 bytes instead of 1, misaligning all
subsequent quest item reads.
TBC 2.4.3 SMSG_CAST_FAILED format is spellId(u32) + result(u8), same as
Classic. WotLK added a castCount(u8) prefix before spellId. TbcPacketParsers
lacked a parseCastFailed override, so it fell through to the WotLK base
which read one extra byte as castCount, shifting the spellId read by one
byte and corrupting the spell ID and result for every failed cast on TBC.
- Add TbcPacketParsers::parseCastFailed override: reads spellId(4)+result(1)
- ClassicPacketParsers already overrides this (enum shift +1), so Classic unaffected
Classic 1.12 auction entries contain only 1 enchant slot (3 uint32s),
while TBC and WotLK expanded this to 3 enchant slots (9 uint32s). Parsing
Classic auction results with the WotLK parser consumed 24 extra bytes per
entry (two extra enchant slots), corrupting randomPropertyId, stackCount,
ownerGuid, pricing and expiry data for every auction item.
- AuctionListResultParser::parse() gains a numEnchantSlots parameter (default 3)
- Classic path reads 1 enchant slot; TBC/WotLK read 3
- handleAuctionListResult/OwnerList/BidderList pass isClassicLikeExpansion()?1:3
Classic 1.12 trainer list entries lack the profDialog and profButton
uint32 fields (8 bytes) that TBC/WotLK added before reqLevel. Instead,
reqLevel immediately follows spellCost, and a trailing unk uint32 appears
at the end of each entry. Parsing the WotLK format for Classic caused
misalignment from the third field onward, corrupting state, cost, level,
skill, and chain data for all trainer spells.
- TrainerListParser::parse() gains a isClassic bool parameter (default false)
- Classic path: cost(4) → reqLevel(1) → reqSkill... → chainNode3 → unk(4)
- WotLK/TBC path: cost(4) → profDialog(4) → profButton(4) → reqLevel(1) → reqSkill...
- handleTrainerList() passes isClassicLikeExpansion() as the flag
Classic 1.12 and TBC use SMSG_FRIEND_LIST (not SMSG_CONTACT_LIST) to send
the initial friend list at login. Previously this packet was silently dropped,
leaving friendsCache empty and breaking /friend remove and note operations
for Classic players.
- Add handleFriendList(): parses Classic format (u8 count, then per-entry:
u64 guid + u8 status + optional area/level/class if online)
- Add handleContactList(): fully parses WotLK SMSG_CONTACT_LIST entries
(previously only read mask+count header and dropped all entries)
- Both handlers populate friendGuids_ and call queryPlayerName() for unknown
GUIDs; handleNameQueryResponse() now backfills friendsCache when a name
resolves for a known friend GUID
- Clear friendGuids_ on disconnect alongside playerNameCache
Vanilla 1.12 SMSG_WHO per-player format:
name(CString) + guild(CString) + level(u32) + class(u32) + race(u32) + zone(u32)
WotLK 3.3.5a added a gender(u8) byte between race and zone. The previous
handleWho always read the gender byte, causing a one-byte misalignment for
Classic/TBC: the first byte of zoneId was consumed as gender, then zoneId
read from the next 4 bytes (spanning into the next player entry).
Now only reads the gender byte for WotLK (isActiveExpansion("wotlk")), and
adds bounds checks to prevent out-of-bounds reads on truncated packets.
Classic 1.12 and TBC 2.4.3 send SMSG_TEXT_EMOTE with the field order:
textEmoteId(u32) + emoteNum(u32) + senderGuid(u64) + nameLen(u32) + name
WotLK 3.3.5a swapped senderGuid to the front:
senderGuid(u64) + textEmoteId(u32) + emoteNum(u32) + nameLen(u32) + name
The previous TextEmoteParser always used the WotLK order, causing senderGuid
to be read as a mashup of textEmoteId+emoteNum for Classic/TBC. Emote
animations and chat entries were associated with wrong GUIDs.
TextEmoteParser::parse now takes a legacyFormat parameter; handleTextEmote
passes it based on expansion detection.
WotLK 3.3.5a added a group-level and per-member roles byte (tank/healer/dps)
for the Dungeon Finder system. Classic 1.12 and TBC 2.4.3 do not send this byte.
The previous GroupListParser always read the roles byte, causing a one-byte
misalignment in Classic/TBC group lists that corrupted member GUID reads and
all subsequent fields (loot method, leader GUID, etc.).
GroupListParser::parse now takes a hasRoles parameter (default true for
backward compatibility). handleGroupList passes hasRoles=isActiveExpansion("wotlk").
Also adds range-checking throughout to prevent out-of-bounds reads on
malformed or unexpectedly short group list packets.
Classic 1.12 servers (vmangos/cmangos-classic) send:
uint64 guid + CString name + CString realmName + uint32 race + uint32 gender + uint32 class
TBC's Variant A (which Classic inherited) skipped the realmName CString,
causing the null terminator of the empty realmName to be absorbed into the
low byte of the uint32 race read, producing race=0 and shifted gender/class.
Adds a ClassicPacketParsers::parseNameQueryResponse override that correctly
reads the realmName CString before the race/gender/class uint32 fields.
Classic/Vanilla uses ObjectGuid::WriteAsPacked() for party member stats
packets (same packed format as WotLK), not full uint64 as TBC does.
Reading 8 fixed bytes for the GUID over-read the packed GUID field,
misaligning updateFlags and all subsequent stat fields, breaking party
frame HP/mana display in Classic.
Classic 1.12 sends packed GUIDs (byte mask + non-zero bytes) for these
server packets, not full uint64 as TBC does. The previous fixes incorrectly
grouped Classic with TBC, causing the GUID readers to over-read 8 bytes
from what were 2-4 byte packed GUIDs, corrupting health values and spell
IDs parsed from subsequent bytes.
Verified from vmangos/cmangos-classic source code:
SMSG_HEALTH_UPDATE: data << GetPackGUID()
SMSG_POWER_UPDATE: data << GetPackGUID()
SMSG_UPDATE_COMBO_POINTS: data << combotarget->GetPackGUID()
SMSG_PERIODICAURALOG: data << victim->GetPackGUID() + caster->GetPackGUID()
SMSG_SPELLENERGIZELOG: data << victim->GetPackGUID() + caster->GetPackGUID()
TBC continues to use full uint64 for these packets. WotLK and Classic
both use packed GUIDs. The branching now correctly distinguishes TBC
from the rest.
Classic 1.12 sends SMSG_AURA_UPDATE/SMSG_AURA_UPDATE_ALL, but ClassicPacketParsers
inherited TBC's override which returns false (TBC uses a different aura system
and doesn't send SMSG_AURA_UPDATE at all).
Classic aura format differs from WotLK in two key ways:
- DURATION flag bit is 0x10 in Vanilla, not 0x20 as in WotLK; reading with the
WotLK parser would incorrectly gate duration reads and misparse aura fields
- No caster GUID field in Classic; WotLK parser tries to read one (gated by 0x08)
which would consume spell ID or flag bytes from the next aura slot
With this override, player/target aura bars and buff tracking work correctly
on Classic 1.12 connections for the first time.
Vanilla 1.12 SMSG_ATTACKERSTATEUPDATE, SMSG_SPELLNONMELEEDAMAGELOG, and
SMSG_SPELLHEALLOG use PackedGuid for all entity GUIDs, not full uint64
as TBC and WotLK do.
Without these overrides Classic inherited TBC's implementations, which
over-read PackedGuid fields as fixed 8-byte GUIDs, misaligning all
subsequent damage/heal fields and making combat parsing unusable on
Classic servers.
The Classic override logic is identical to TBC except for the GUID
reads, so combat text, damage numbers, and kill tracking now work
correctly on Vanilla 1.12 connections.
Vanilla 1.12 SMSG_SPELL_START and SMSG_SPELL_GO use:
- PackedGuid (variable-length) for caster and target GUIDs, not full uint64
- uint16 castFlags, not uint32 as in TBC/WotLK
- uint16 targetFlags in SpellCastTargets, not uint32
Without these overrides Classic inherited TBC's implementations which
read 8 bytes for each GUID (over-reading the PackedGuid) and then 4
bytes for castFlags instead of 2, misaligning all subsequent fields
and producing garbage spell IDs, cast times, and target GUIDs.
Hit and miss target GUIDs in SMSG_SPELL_GO are also PackedGuid in
Vanilla (vs full uint64 in TBC), handled by the new parseSpellGo.
Previously SMSG_SPELL_DELAYED only adjusted the local player's cast bar.
Now it also extends unitCastStates_ for any non-player caster (e.g.
boss cast bar extends correctly when hit by a tank during cast).
- Handle MSG_CHANNEL_START: populate unitCastStates_ for both the local
player and any non-player caster (boss/mob channeled spells); use
full uint64 GUIDs for TBC/Classic, packed GUIDs for WotLK
- Handle MSG_CHANNEL_UPDATE: sync remaining channel time; clear cast
state on channel completion (remainingMs == 0)
- Fix SMSG_RESUME_CAST_BAR: also resumes non-player units' cast bars
(previously only resumed the player's own bar after zone transitions)
- Add party member cast bars in renderPartyFrames: golden progress bar
appears beneath the power bar when a party member is casting,
leveraging the existing unitCastStates_ per-GUID map
WotLK uses packed GUIDs in SMSG_SPELL_FAILURE / SMSG_SPELL_FAILED_OTHER.
TBC 2.4.3 and Classic 1.12 use full uint64 GUIDs. The previous fix used
UpdateObjectParser::readPackedGuid for all expansions, which would
mis-parse the caster GUID on TBC/Classic servers, leaving stale cast
bars and potentially corrupting subsequent packet reads.
Now checks isClassicLikeExpansion() || isActiveExpansion("tbc") and
reads a raw uint64 for those expansions, matching the TBC/Classic wire
format used in parseSpellStart/parseSpellGo overrides.
When a spell fails or is interrupted, the server sends SMSG_SPELL_FAILURE
(for the caster's own POV) or SMSG_SPELL_FAILED_OTHER (for observers).
Previously these were consumed without updating cast state, leaving stale
cast bars for interrupted enemies. Now:
- SMSG_SPELL_FAILURE: erases unitCastStates_[failGuid] for non-player
casters (still clears player casting/currentCastSpellId for own casts)
- SMSG_SPELL_FAILED_OTHER: erases unitCastStates_[guid] for the caster
so boss/enemy cast bars immediately clear on interrupt/kick
Previously the target cast bar tracked a single target using 4 private
fields. This replaces that with unitCastStates_ (unordered_map<uint64_t,
UnitCastState>), tracking cast state for every non-player unit whose
SMSG_SPELL_START we receive.
Changes:
- GameHandler::UnitCastState struct: casting, spellId, timeRemaining,
timeTotal
- getUnitCastState(guid) → returns cast state for any tracked unit
- isTargetCasting(), getTargetCastSpellId(), getTargetCastProgress(),
getTargetCastTimeRemaining() now delegate to getUnitCastState(targetGuid)
- handleSpellStart: tracks all non-player casters (not just the target)
- handleSpellGo: erases caster from map when spell lands
- update loop: ticks down all unit cast states, erasing expired entries
- unitCastStates_ cleared on world reset
- renderBossFrames: shows red cast progress bar per boss slot with
spell name + remaining seconds — critical for instance interrupt play
Adds 5 gold/grey dot indicators below the power bar in the player frame
for Rogue (class 4) and Druid (class 11), showing the current combo
point count from SMSG_UPDATE_COMBO_POINTS. Active points are bright gold;
empty slots are dark grey. Dots are centered in the frame width.
The display is always shown for Rogues; for Druids it only appears when
combo points are non-zero (they only accumulate in Cat Form).
SMSG_SPELL_START fires for all units, not just the player. Previously only
the player's own cast was tracked; now we also track when the current
target is casting, enabling interrupt decisions.
- GameHandler: track targetCasting_/targetCastSpellId_/targetCastTimeTotal_
/targetCastTimeRemaining_ — updated by SMSG_SPELL_START for the current
target and ticked down in the update loop each frame
- Target cast cleared when: target changes (setTarget), target's spell
lands (SMSG_SPELL_GO), or cast timer expires naturally
- game_screen: renderTargetFrame shows a red cast progress bar between
the power bar and distance line when the target is casting, with
spell name + remaining seconds
- Public accessors: isTargetCasting(), getTargetCastSpellId(),
getTargetCastProgress(), getTargetCastTimeRemaining()
Both SpellGoParser::parse (WotLK) and TbcPacketParsers::parseSpellGo
(TBC) read missCount but did not consume the per-miss (guid + missType)
entries that follow, leaving unread bytes in the packet and silently
corrupting any subsequent parsing of cast-flags–gated spell data.
- Add SpellGoMissEntry{targetGuid, missType} and missTargets vector
to SpellGoData
- WotLK parser now reads packed GUIDs + missType per miss entry
- TBC parser now reads full uint64 GUIDs + missType per miss entry
(9 bytes per entry, bounds-checked)
- handleSpellGo now shows MISS/DODGE/PARRY/BLOCK combat text
for each missed target when the local player cast the spell,
complementing the existing SMSG_SPELLLOGMISS path
- Remove unused foliageLikeModel variable in m2_renderer pass-2 loop
(fix unused-variable warning)
- Update smoke model comment in m2_renderer to reflect current state
SMSG_PET_SPELLS: Parse full packet — pet GUID, react/command state,
10 action bar slots, per-spell entries with autocast flags. Previously
only read the GUID.
SMSG_PET_MODE: Parse petGuid + mode uint32 (command low byte, react
high byte) to keep stance state in sync after server updates.
SMSG_PET_BROKEN: Clear pet state and show "Your pet has died." chat
message.
SMSG_PET_LEARNED_SPELL / SMSG_PET_UNLEARNED_SPELL: Maintain pet spell
list incrementally.
SMSG_PET_CAST_FAILED: Parse and log cast count + spell + reason.
New state accessors: getPetActionSlot(), getPetCommand(), getPetReact(),
getPetSpells(), isPetSpellAutocast().
CMSG_PET_ACTION: Add targetGuid (uint64) field — the wire format
requires petGuid(8)+action(4)+targetGuid(8). Was sending an 12-byte
packet instead of the required 20 bytes.
sendPetAction(): New method that builds and sends CMSG_PET_ACTION with
the correct target guid.
switchTalentSpec() was only updating local state without notifying the
server, leaving the server out of sync with the client's active talent
group. Now sends CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE (WotLK wire
opcode 0x4C3) with the target group index (0=primary, 1=secondary),
prompting the server to apply the spec swap and respond with a fresh
SMSG_TALENTS_INFO for the newly active group.
Also adds ActivateTalentGroupPacket::build() to world_packets for the
packet construction.
The SMSG_LFG_PLAYER_REWARD handler was printing raw copper value with
a "g" suffix (e.g. "12345g") instead of converting to gold/silver/copper.
Now formats as "1g 23s 45c" matching the standard WoW convention.
Renders a compact top-centre overlay showing Alliance vs Horde scores
when the player is in a recognised battleground map. Score values are
read directly from the world state map maintained by SMSG_INIT_WORLD_STATES
and SMSG_UPDATE_WORLD_STATE, so no extra server packets are needed.
Supported maps:
489 – Warsong Gulch (flag captures, max 3)
529 – Arathi Basin (resources, max 1600)
30 – Alterac Valley (reinforcements, max 600)
566 – Eye of the Storm (resources, max 1600)
607 – Strand of Ancients
TBC 2.4.3 and Classic 1.12 send resetTime as uint32 (seconds) with no
extended byte, while WotLK 3.3.5a sends uint64 timestamp + extended byte.
Parse the correct field widths based on expansion to prevent corrupted
instance lockout data on TBC/Classic realms.
When a player releases spirit, the server sends SMSG_DEATH_RELEASE_LOC
with the corpse map and position. Store this so the ghost can reclaim.
New flow:
- SMSG_DEATH_RELEASE_LOC now stores corpseMapId_/corpseX_/Y_/Z_ instead
of logging and discarding
- canReclaimCorpse(): true when ghost is on same map within 40 yards of
stored corpse position
- reclaimCorpse(): sends CMSG_RECLAIM_CORPSE (no payload)
- renderReclaimCorpseButton(): shows "Resurrect from Corpse" button at
bottom-center when canReclaimCorpse() is true
Two bugs fixed:
1. acceptResurrect() was always sending CMSG_SPIRIT_HEALER_ACTIVATE even
for player-cast resurrections (Priest/Paladin/Druid). That opcode is
only the correct response to SMSG_SPIRIT_HEALER_CONFIRM. For
SMSG_RESURRECT_REQUEST the server expects CMSG_RESURRECT_RESPONSE
with accept=1. Added resurrectIsSpiritHealer_ to track which path
triggered the dialog and send the right packet per type.
2. The resurrect dialog showed a generic "Return to life?" string
regardless of who cast the resurrection. Parse the optional CString
name from SMSG_RESURRECT_REQUEST (or fall back to playerNameCache)
and display "X wishes to resurrect you." when the caster is known.
TBC 2.4.3 does not have SMSG_AURA_UPDATE (added in WotLK). Instead it
uses SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE (0x3A3) for full aura refresh
on login/zone and SMSG_SET_EXTRA_AURA_INFO_OBSOLETE (0x3A4) for single-
slot updates. Implement handlers for both packets so TBC buff/debuff bars
populate correctly.
Also implement SMSG_CLEAR_EXTRA_AURA_INFO (0x3A6) to remove individual
aura slots when buffs expire or are cancelled server-side.
Format parsed: uint64 targetGuid + uint8 count + per-slot {uint8 slot,
uint32 spellId, uint8 effectIndex, uint8 flags, uint32 durationMs,
uint32 maxDurationMs}. Infinite auras (0xFFFFFFFF) stored as durationMs=-1.
Single shadow depth image shared across MAX_FRAMES=2 in-flight GPU frames
caused a race: frame N's main pass reads shadow map while frame N+1's
shadow pass clears and writes it, producing visible flashing standing
still and while moving.
Fix: give each in-flight frame its own VkImage, VmaAllocation, VkImageView,
and VkFramebuffer for the shadow depth attachment. renderShadowPass() now
indexes all shadow resources by getCurrentFrame(), and layout transitions
track per-frame state in shadowDepthLayout_[frame]. Cleanup loops over
MAX_FRAMES=2. Descriptor sets already written per-frame; updated shadow
image view binding to use the matching per-frame view.
TBC 2.4.3 quest log update fields use 4 fields per slot
(questId, state, counts, timer) vs WotLK's 5 (extra counts field).
The wrong stride (5) caused all quest log reads to use wrong indices
beyond the first slot, breaking quest tracking on TBC servers.
TBC 2.4.3 CMSG_QUESTGIVER_QUERY_QUEST is guid(8) + questId(4) = 12
bytes. WotLK added a trailing isDialogContinued(u8) byte that TBC
servers don't expect; sending it caused quest details to not be sent
back on some emulators.
When the player starts casting Hearthstone (spell IDs 6948/8690),
trigger background terrain loading at the bind point so tiles are
ready when the teleport fires.
- Add HearthstonePreloadCallback to GameHandler, called from
handleSpellStart when a Hearthstone cast begins.
- Application callback enqueues a 5×5 tile grid around the bind
point via precacheTiles() (same-map) or starts a file-cache warm
via startWorldPreload() (cross-map) during the ~10 s cast time.
- On same-map teleport arrival, call processAllReadyTiles() to
GPU-upload any tiles that finished parsing during the cast before
the first frame at the new position.
Fixes: player landing in unloaded terrain and falling after Hearthstone.
TBC 2.4.3 SMSG_SPELL_START and SMSG_SPELL_GO send full uint64 GUIDs for
casterGuid/casterUnit and hit targets. WotLK uses packed (variable-length)
GUIDs. Using readPackedGuid() on a full uint64 reads the first byte as the
bitmask, consuming 1-8 wrong bytes, which shifts all subsequent fields
(spellId, castFlags, castTime) and causes:
- Cast bar to never show for the player's own spells
- Sound effects to use the wrong spell ID
- Hit/miss target tracking to be completely wrong
Additionally, TBC SMSG_SPELL_GO lacks the WotLK timestamp field after
castFlags.
Add TbcPacketParsers::parseSpellStart and ::parseSpellGo using full GUIDs,
add virtual base methods, and route both handlers through virtual dispatch.
TBC 2.4.3 SMSG_CAST_RESULT sends spellId(u32) + result(u8) = 5 bytes.
WotLK 3.3.5a added a castCount(u8) prefix making it 6 bytes. Without
this fix the WotLK parser was reading spellId[0] as castCount, then the
remaining 3 spellId bytes plus result byte as spellId (wrong), and then
whatever follows as result — producing incorrect failure messages and
potentially not clearing the cast bar on TBC.
Add TbcPacketParsers::parseCastResult override and a virtual base method,
then route SMSG_CAST_RESULT through virtual dispatch in the game handler.