Add GhostStateCallback to GameHandler, fired when PLAYER_FLAGS_GHOST
transitions on or off in UPDATE_OBJECT / login detection. Add
setInstanceOpacity() to CharacterRenderer to directly set opacity
without disturbing fade-in state. Application wires the callback to
set opacity 0.5 on ghost entry and 1.0 on resurrect.
Add StandStateCallback to GameHandler, fired when the server confirms
a stand state change (SMSG_STANDSTATE_UPDATE). Connect in Application
to map the WoW stand state (0-8) to M2 animation IDs on the player
character model:
- 0 = Stand → anim 0 (Stand)
- 1-6 = Sit variants → anim 27 (SitGround)
- 7 = Dead → anim 1 (Death)
- 8 = Kneel → anim 72 (Kneel)
Sit and Kneel animations are looped so the held-pose frame stays
visible; Death stays on the final frame.
Add SpellCastAnimCallback to GameHandler, triggered on SMSG_SPELL_START
(start=true) and cleared on SMSG_SPELL_GO / SMSG_SPELL_FAILURE
(start=false) for both the player and other units.
Connect the callback in Application to play animation 3 (SpellCast) on
the player character, NPCs, and other players when they begin a cast.
The cast animation is one-shot (loop=false) so it auto-returns to Stand
when complete via the existing return-to-idle logic.
Also fire stop-cast on spell failure to cancel any stuck cast pose.
Remove active file-I/O debug block in character_renderer.cpp that
wrote composite textures as raw binary files to /tmp on every texture
composite generation. Remove the now-unused <fstream> include.
Remove the 10-shot hex dump of decompressed SMSG_MONSTER_MOVE payloads
in game_handler.cpp (dumpCount static); format has been confirmed.
pendingCreatureQueries and pendingGameObjectQueries_ were never cleared on
disconnect. If a query was sent but the response lost (e.g. server
disconnect mid-flight), the entry would remain in the pending set after
reconnect, causing queryCreatureInfo/queryGameObjectInfo to silently skip
re-issuing the query — leaving NPC and GO names unpopulated for those
entries.
Clear both sets on disconnect so reconnect sees them as unqueried and
re-sends the queries as needed. creatureInfoCache/gameObjectInfoCache_ are
intentionally preserved across sessions to avoid re-querying entries whose
responses did arrive.
Every GameObject CREATE block was logged at WARNING level, spamming the
warning log with doors, chests, and other world objects. Demote to DEBUG
since this is routine spawn traffic; keep transport detection at INFO since
those are noteworthy.
Previously, if any single block in an SMSG_UPDATE_OBJECT packet failed
to parse (e.g. unusual spline flags), the entire packet was dropped and
all entities in it were lost. On busy zones with many CREATE_OBJECTs in
one packet, one bad NPC movement block would silently suppress all NPCs
that followed it in the same packet.
- parseUpdateObject: break instead of return false on block failure,
so already-parsed blocks are returned to the caller
- handleUpdateObject: fall through to process partial data when parsing
returns false but some blocks were successfully parsed
On disconnect/reconnect to the same map, entityManager was not cleared
and creatureInstances_ still held old entries from the previous session.
When the server re-sent CREATE_OBJECT for the same GUIDs, the spawn
callback's early-return guard (creatureInstances_.count(guid)) silently
dropped every NPC re-spawn, leaving the world empty.
Fixes:
- disconnect() now calls entityManager.clear() to purge stale entities
- WorldEntryCallback gains a bool isInitialEntry parameter (true on first
login or reconnect, false on in-world teleport/flight landing)
- Same-map optimization path skipped when isInitialEntry=true, so
loadOnlineWorldTerrain runs its full cleanup and properly despawns old
creature/player instances before the server refreshes them
- Load WorldMapArea.dbc lazily on first use to build areaId→name lookup
- /who results now show [Zone Name] alongside level: 'Name - Level 70 [Stormwind City]'
- SMSG_EXPLORATION_EXPERIENCE now shows 'Discovered Elwynn Forest! Gained X experience.'
instead of generic 'Discovered new area!' message when the zone name is available
- Cache is populated once per session and shared across both callsites
Quest kill count tracker in the HUD now resolves creature names from the
cached creature query results and displays them as "Name: x/y" instead
of bare "x/y". The system chat progress message on kill also includes
the creature name when available, matching retail client behavior.
Replace static-local firstSpecReceived with talentsInitialized_ member
variable, reset in handleLoginVerifyWorld alongside other per-session
state. Also clear learnedTalents_, unspentTalentPoints_, and
activeTalentSpec_ at world entry so reconnects and character switches
start from a clean talent state instead of carrying over stale data.
- Track PLAYER_REST_STATE_EXPERIENCE update field for all expansions
(WotLK=636, Classic=718, TBC=928, Turtle=718)
- Set isResting_ flag from SMSG_SET_REST_START packet
- XP bar shows rested bonus as a lighter purple overlay extending
beyond the current fill to (currentXp + restedXp) position
- Tooltip text changes to "%u / %u XP (+%u rested)" when bonus exists
- "zzz" indicator shown at bar right edge while resting
- Nameplates: player names always rendered regardless of V-key toggle;
separate cull distance 40u (players/target) vs 20u (NPCs); cyan name
color for other players; fade alpha scales with cull distance
- Level-up: add expanding golden ring burst (3 staggered waves, 420u
max radius) + full-screen flash to renderDingEffect(); M2 LevelUp.m2
is still attempted as a bonus on top
- Vanilla tile loading: add AssetManager::setBaseFallbackPath() so that
when the primary manifest is an expansion-specific DBC-only subset
(e.g. Data/expansions/vanilla/), world terrain files fall back to
the base Data/ extraction; wired in Application::initialize()
- Warden: map a null guard page at address 0x0 in the Unicorn emulator
so NULL-pointer reads in the module don't crash with UC_ERR_MAP;
execution continues past the NULL read for better diagnostics
- game: clear pendingNameQueries on player out-of-range and DESTROY_OBJECT so
re-entering players get a fresh name query instead of being silently skipped
- game: add 5s periodic name resync scan that re-queries players with empty names
and no pending query, recovering from dropped CMSG_NAME_QUERY responses
- warden: fix UC_ERR_MAP by moving HEAP_BASE from 0x200000 to 0x20000000; the old
heap [0x200000, 0x1200000) overlapped the module at 0x400000, causing Unicorn to
reject the heap mapping and abort emulator initialisation
- warden: add early overlap check between module and heap regions to catch future
layout bugs at init time
- assets: add loadDBCOptional() which logs at DEBUG level when a DBC is absent,
for files that are not distributed on all expansions
- assets: use loadDBCOptional for Item.dbc (absent on Vanilla 1.12 clients) and
fall back to server-sent itemInfoCache displayInfoId for NPC weapon resolution
setTarget() was not clearing targetAuras, leaving stale buff/debuff
icons from the previous target visible on the buff bar until the server
sent SMSG_AURA_UPDATE_ALL for the new target. Reset all slots to empty
on target change so the display is immediately correct.
handleDestroyObject invoked creatureDespawnCallback_ and
gameObjectDespawnCallback_ but not playerDespawnCallback_ for PLAYER
entities. This caused the CharacterRenderer instance for nearby players
to remain alive after they received a DESTROY_OBJECT packet (e.g. when
they teleported or went out of range via server-forced despawn), leaving
phantom models in the world.
Mirror the same despawn logic used for out-of-range removal: call
playerDespawnCallback_ and clean up the per-player bookkeeping maps so
the renderer cleans up the character instance correctly.
SMSG_GROUP_LIST is a full replacement packet, not a delta. handleGroupList()
was not resetting partyData before parsing, so repeated GROUP_LIST packets
pushed duplicate members onto the existing vector — a 2-player party would
show the same name 5 times if the packet was sent 5 times.
Fix: reset partyData = GroupListData{} before calling GroupListParser::parse().
Also fix player names staying "Unknown" when an entity moves out of range and
comes back: queryPlayerName() now applies the cached name to the new entity
object immediately instead of skipping when the name is already in cache.
This was causing other players' names to appear as unknown after zoning or
crossing render distance boundaries.
Parse the full and single-update variants of MSG_RAID_TARGET_UPDATE to
track which guid carries each of the 8 raid icons (Star/Circle/Diamond/
Triangle/Moon/Square/Cross/Skull). Marks are cleared on world transfer.
The target frame now shows the Unicode symbol for the target's raid mark
in its faction color to the left of the name. Nameplates show the same
symbol to the left of the unit name for all nearby marked units.
Expand action bar from 12 to 24 slots (2 bars × 12). Bar 2 is rendered
above bar 1 and loaded from SMSG_ACTION_BUTTONS slots 12-23. Pressing
Shift+number activates the corresponding bar-2 slot. Drag-and-drop,
cooldown overlays, and tooltips work identically on both bars. Bar 2
fades slightly when all its slots are empty to minimize visual noise.
When a friend goes online, offline, or is added/removed, update the
contacts_ vector in addition to friendsCache. This ensures the Friends
tab in the Social window always reflects the current state without
needing a full SMSG_CONTACT_LIST/SMSG_FRIEND_LIST refresh.
Store structured friend data (online status, level, area, class) that
was previously discarded in handleFriendList/handleContactList. New
ContactEntry struct lives in game_handler.hpp; getContacts() exposes it.
UI: the O-key Social window (formerly guild-only) now has a Friends tab.
- Shows online/offline status dot, name, level, and AFK/DND label
- Pressing O when not in a guild opens Social directly on the Friends tab
- The window title changed from "Guild" to "Social" for accuracy
- Non-guild players no longer get a "not in a guild" rejection on O press
Demote parse-level diagnostic logs that fire on every game interaction:
- TBC/Classic gossip, quest details, quest rewards: LOG_INFO → LOG_DEBUG
- WotLK gossip, quest details/reward/request-items: LOG_INFO → LOG_DEBUG
- Attack start/stop, XP gain, loot, name query, vendor, party: LOG_INFO → LOG_DEBUG
- TBC SMSG_UPDATE_OBJECT has_transport fallback: LOG_WARNING → LOG_DEBUG
- TBC parseAuraUpdate not-in-TBC diagnostic: LOG_WARNING → LOG_DEBUG
- Turtle SMSG_MONSTER_MOVE WotLK fallback: LOG_WARNING → LOG_DEBUG
These all fire multiple times per second during normal gameplay.
- world_packets.cpp::InitialSpellsParser::parse already logs spell count
at LOG_INFO; remove the duplicate count from handleInitialSpells()
- Downgrade verbose format-detection LOG_INFO to LOG_DEBUG (packet size,
format name, first-10 spell IDs) — these are diagnostic details that
clutter INFO output without adding operational value
- AUTH HASH logs (sessionKey, hash input, digest): session key material
must never appear in production logs at INFO level — downgrade to DEBUG
- SMSG_AUTH_CHALLENGE field details (seeds, unknown1): downgrade to DEBUG;
keep one INFO line with format name for connection diagnostics
- SMSG_MOTD per-line content: downgrade to DEBUG; keep INFO line count
- Transport position update per-entity: fires on every update for each
entity riding a transport — downgrade to DEBUG
queryItemInfo and handleItemQueryResponse fire for every item in
inventory, loot windows, vendor lists, and mail — potentially dozens
of times at login or when any container is opened. Downgrade to
LOG_DEBUG to reduce noise. Also downgrade useItemById search traces
to LOG_DEBUG; the final warning (item not found) stays at LOG_WARNING.
Debug-labeled LOG_INFO calls in handleTrainerList and handleInitialSpells
fire every time the trainer window opens or the player logs in, producing
noisy output that obscures meaningful events.
- handleTrainerList: known spells list dump, hardcoded prerequisite checks
(527/25312), and per-spell detail lines → LOG_DEBUG
Keep one LOG_INFO for the spell count summary (meaningful lifecycle event)
- handleInitialSpells: hardcoded spell presence checks (527/988/1180) →
LOG_DEBUG; replace with a single LOG_INFO for spell count summary
TBC 2.4.3 and Classic 1.12 share the same SMSG_QUESTGIVER_QUEST_DETAILS
format. WotLK 3.3.5a adds three extra fields (informUnit u64, flags u32,
isFinished u8) that the base QuestDetailsParser::parse handles. TBC had no
override, so it fell through to the WotLK heuristic which read flags+isFinished
as if they were TBC fields, misaligning choiceCount, rewardMoney, and rewardXp.
Fix: move parseQuestDetails from ClassicPacketParsers to TbcPacketParsers.
Classic inherits it unchanged (formats are identical). Both expansions now
correctly parse: no informUnit, activateAccept(u8), suggestedPlayers(u32),
emote section, variable choice/reward item counts, rewardMoney, and rewardXp.
Consistent with the same cleanup for Classic/TBC parsers. Melee hit, spell
damage, and spell heal logs fire on every combat event and belong at DEBUG
level. Per-character detail lines in the WotLK CharEnum parser are also
consolidated to a single DEBUG line per character.
Combat event logs (melee hit, spell damage, spell heal) fire on every combat
event and should be DEBUG-level. The per-character detail lines in parseCharEnum
are also moved to DEBUG — the summary line stays at INFO.
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.
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.
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.