- Add getServerTurnRate() accessor and turnRateOverride_ field so the
keyboard turn speed respects SMSG_FORCE_TURN_RATE_CHANGE from server
- Convert rad/s → deg/s before applying to camera yaw logic
- Fix SMSG_SPLINE_SET_RUN_BACK/SWIM/FLIGHT/FLIGHT_BACK/SWIM_BACK/WALK/
TURN_RATE handlers: all previously discarded the value; now update the
corresponding serverXxxSpeed_ / serverTurnRate_ field when GUID matches
playerGuid (camera controller syncs these every frame)
SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE was already ACK'd and stored in
serverFlightBackSpeed_, but the value was never accessible or synced
to the CameraController. Backward flight movement always used forward
flight speed (flightSpeedOverride_), making it faster than the server
intended.
- Add getServerFlightBackSpeed() accessor in GameHandler
- Add flightBackSpeedOverride_ field and setter in CameraController
- Apply it in the fly movement block: backward-only flight uses the
back speed; forward or strafing uses the forward speed as WoW does
- Fallback: 50% of forward flight speed when override is unset
- Sync per-frame in application.cpp alongside the other speed overrides
Backward swimming was using 50% of forward swim speed as a hardcoded
fallback. Wire up the server-authoritative swim back speed so Warlock
Dark Pact, buffs, and server-forced speed changes all apply correctly
when swimming backward.
- game_handler.hpp: add getServerSwimBackSpeed() accessor
- camera_controller.hpp: add swimBackSpeedOverride_ field + setter
- camera_controller.cpp: apply swimBackSpeedOverride_ when player
swims backward without forward input; fall back to 50% of swim speed
- application.cpp: sync swim back speed each frame
When the server sets MovementFlags::HOVER (SMSG_MOVE_SET_HOVER), the
player now floats 4 yards above the nearest ground surface instead of
standing on it. Uses the existing floor-snap path with a HOVER_HEIGHT
offset applied to the snap target.
- game_handler.hpp: add isHovering() accessor (reads HOVER flag from
movementInfo.flags, which is already set by handleForceMoveFlagChange)
- camera_controller.hpp: add hoverActive_ field and setHoverActive()
- camera_controller.cpp: apply HOVER_HEIGHT = 4.0f offset at floor snap
- application.cpp: sync hover state each frame alongside other movement
states (gravity, feather fall, water walk, flying)
Previously only run speed was synced. Now all server-driven movement
speeds are forwarded to the camera controller each frame:
- runSpeedOverride_: server run speed (existing)
- walkSpeedOverride_: server walk speed (Ctrl key movement)
- swimSpeedOverride_: swim speed (Swim Form, Engineering fins)
- flightSpeedOverride_: flight speed (epic vs normal flying mounts)
- runBackSpeedOverride_: back-pedal speed
Each uses the server value when non-zero/sane, falling back to the
hardcoded WoW default constant otherwise.
serverFlightSpeed_ (from SMSG_FORCE_FLIGHT_SPEED_CHANGE) was stored but
never synced to CameraController. Add getServerFlightSpeed() accessor,
flightSpeedOverride_ field, and use it in the flying physics path so
normal vs epic flying mounts actually move at their correct speeds.
When CAN_FLY + FLYING movement flags are both set (flying mounts, Druid
Flight Form), the CameraController now uses 3D pitch-following movement
instead of ground physics:
- Forward/back follows the camera's 3D look direction (ascend when
looking up, descend when looking down)
- Space = ascend vertically, X (while mounted) = descend
- No gravity, no grounding, no jump coyote time
- Fall-damage checks suppressed (grounded=true)
Also wire up all remaining server movement state flags to CameraController:
- Feather Fall: cap terminal velocity at -2 m/s
- Water Walk: clamp to water surface, skip swim entry
- Flying: 3D movement with no gravity
All states synced each frame from GameHandler via isPlayerFlying(),
isFeatherFalling(), isWaterWalking(), isGravityDisabled().
SMSG_MOVE_WATER_WALK / SMSG_MOVE_LAND_WALK now correctly set/clear
WATER_WALK (0x00008000) in movementInfo.flags, ensuring the flag is
included in movement ACKs sent to the server.
In CameraController, when waterWalkActive_ is set and the player is
at or above the water surface (within 0.5 units), clamp them to the
water surface and mark as grounded — preventing water entry and allowing
them to walk across the water surface as the spell intends.
Feather Fall (SMSG_MOVE_FEATHER_FALL / SMSG_MOVE_NORMAL_FALL):
- Add FEATHER_FALL = 0x00004000 to MovementFlags enum
- Fix handlers to set/clear the flag instead of passing flag=0
- Cap downward terminal velocity at -2.0 m/s in CameraController when
feather fall is active (Slow Fall, Parachute, etc.)
All three handlers now correctly propagate server movement state flags
that were previously acknowledged without updating any local state.
serverWalkSpeed_ and serverSwimSpeed_ were stored in GameHandler but
never exposed or synced to the camera controller. The controller used
hardcoded WOW_WALK_SPEED and speed*SWIM_SPEED_FACTOR regardless of
server-sent speed changes.
Add getServerWalkSpeed()/getServerSwimSpeed() accessors, walkSpeedOverride_
and swimSpeedOverride_ fields in CameraController, and sync all three
server speeds each frame. Both swim speed sites (main and camera-collision
path) now use the override when set. This makes Slow debuffs (walk speed),
Swim Form, and Engineering fins actually affect movement speed.
SMSG_MOVE_GRAVITY_DISABLE/ENABLE now correctly set/clear the LEVITATING
movement flag instead of passing flag=0. GameHandler::isGravityDisabled()
reads the LEVITATING bit and is synced to CameraController each frame.
When gravity is disabled the physics loop bleeds off downward velocity
and skips gravity accumulation, so Levitate and similar effects actually
float the player rather than letting them fall through the world.
When SMSG_FORCE_MOVE_ROOT sets ROOT in movementInfo.flags, the
camera controller was not aware and continued to accept directional
input. This caused position desync (client moves, server sees player
as rooted).
- Add movementRooted_ flag to CameraController with setter/getter.
- Block nowForward/nowBackward/nowStrafe when movementRooted_ is set.
- Sync isPlayerRooted() from GameHandler to CameraController each
frame alongside the existing run-speed sync in application.cpp.
- Add GameHandler::isPlayerRooted() convenience accessor.
- SMSG_IGNORE_LIST was silently consumed; now parses guid+name pairs to
populate ignoreCache so /unignore works correctly for pre-existing
ignores loaded at login.
- MSG_TALENT_WIPE_CONFIRM was discarded without responding; now parses
the NPC GUID and cost, shows a confirm dialog, and sends the required
response packet when the player confirms. Without this, talent reset
via Talent Master NPC was completely broken.
Previously movementInfo.fallTime was always 0 and jumpVelocity/jumpSinAngle/
jumpCosAngle/jumpXYSpeed were never populated. The server reads fallTime
unconditionally from every movement packet and uses it to compute fall damage
and anti-cheat heuristics; the jump fields are required when FALLING is set.
Changes:
- Add isFalling_ / fallStartMs_ to track fall state across packets
- MSG_MOVE_JUMP: set isFalling_=true, record fallStartMs_, populate jump fields
(jumpVelocity=7.96, direction from facing angle, jumpXYSpeed from server
run speed or walk speed when WALKING flag is set)
- MSG_MOVE_FALL_LAND: clear all fall/jump fields
- sendMovement: update movementInfo.fallTime = (time - fallStartMs_) each call
so every heartbeat and position packet carries the correct elapsed fall time
- World entry: reset all fall/jump fields alongside the flag reset
Previously the handler ACKed with current position and ignored the
velocity fields entirely (vcos/vsin/hspeed/vspeed were [[maybe_unused]]).
The server expects the client to fly through the air on knockback — without
simulation the player stays in place while the server models them as airborne,
causing position desync and rubberbanding.
Changes:
- CameraController: add applyKnockBack(vcos, vsin, hspeed, vspeed)
that sets knockbackHorizVel_ and launches verticalVelocity = -vspeed
(server sends vspeed as negative for upward launches, matching TrinityCore)
- Physics loop: each tick adds knockbackHorizVel_ to targetPos then applies
exponential drag (KNOCKBACK_HORIZ_DRAG=4.5/s) until velocity < 0.05 u/s
- GameHandler: parse all four fields, add KnockBackCallback, call it for
the local player so the camera controller receives the impulse
- Application: register the callback — routes server knockback to physics
The existing ACK path is unchanged; the server gets position confirmation
as before while the client now actually simulates the trajectory.
CMSG_LFG_SET_BOOT_VOTE was defined in the opcode table but never sent.
- Add GameHandler::lfgSetBootVote(bool) which sends the packet
- Fix handleLfgBootProposalUpdate() to set lfgState_=Boot while the
vote is in progress and return to InDungeon when it ends
- Add Yes/No vote buttons to the Dungeon Finder window when in Boot state
These three server-push opcodes were silently consumed without sending
the required client acks, causing the server to stall waiting for
confirmation before granting the capability.
- SMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY →
CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK (via handleForceMoveFlagChange)
- SMSG_MOVE_UNSET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY →
same ack opcode (no separate unset ack exists in WotLK 3.3.5a)
- SMSG_MOVE_SET_COLLISION_HGT → CMSG_MOVE_SET_COLLISION_HGT_ACK via new
handleMoveSetCollisionHeight() which appends the float height after the
standard movement block (required by server-side ack validation)
Add UnitMoveFlagsCallback fired on every MSG_MOVE_* with the raw
movement flags field. Application.cpp uses it to update swimming
and walking state from any packet, not just explicit START_SWIM/
STOP_SWIM opcodes — fixing cold-join cases where a player is already
swimming when we enter the world.
Per-frame animation sync now selects Walk(4) when the WALKING flag is
set, Run(5) otherwise, and Swim(42)/SwimIdle(41) when swimming.
UnitAnimHintCallback is simplified to jump (38=JumpMid) only.
- MSG_MOVE_STOP/STOP_STRAFE/STOP_TURN/STOP_SWIM/FALL_LAND: snap entity to
stop position (duration=0) and pass durationMs=0 to renderer so the
Run-animation flash is suppressed; per-frame sync plays Stand on next frame
- MSG_MOVE_JUMP: fire new UnitAnimHintCallback with anim 38 (JumpMid) so
other players and NPCs visually leave the ground during jumps
- MSG_MOVE_START_SWIM: fire UnitAnimHintCallback with anim 42 (Swim)
- Wire up UnitAnimHintCallback in application.cpp; skips Death (anim 1)
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.
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
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
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.
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
Hearthstone post-teleport fix:
- Expand same-map hearthstone precache from 5x5 to 9x9 tiles so workers
have more tiles parsed before the player arrives at the bind point
- After same-map teleport arrival, enqueue the full load-radius tile grid
(17x17 = 289 tiles) at the new position so background workers immediately
start loading all WMOs/M2s visible from the new location
Quest tracker improvements:
- Clicking a quest in the tracker now opens the Quest Log (L)
- Remove NoInputs flag so the tracker window receives mouse events
- Show only tracked quests in tracker; fall back to all quests if none tracked
- Add Track/Untrack button in Quest Log details panel
- Abandoning a quest automatically untracks it
- Track state stored in GameHandler::trackedQuestIds_ (per-session)
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
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
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()
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.
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.
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.
- Parse uint8 itemClass + uint32 subClassMask from SMSG_SET_PROFICIENCY
- Store weaponProficiency_ (itemClass=2) and armorProficiency_ (itemClass=4)
- Expose getWeaponProficiency(), getArmorProficiency(), canUseWeaponSubclass(n),
canUseArmorSubclass(n) on GameHandler for use by equipment UI
- Enables future equipment slot validation (grey out non-proficient items)
Socket fixes (fixes Windows-only connection failure):
- WorldSocket::connect() now waits for non-blocking connect to complete with
select() before returning, preventing WSAENOTCONN on the first recv() call
on Windows (Linux handles this implicitly but Windows requires writability
poll after non-blocking connect)
- Add net::isConnectionClosed() helper: treats WSAENOTCONN/WSAECONNRESET/
WSAESHUTDOWN/WSAECONNABORTED as graceful peer-close rather than recv errors
- Apply isConnectionClosed() in both WorldSocket and TCPSocket recv loops
UI:
- Add renderBossFrames(): displays boss unit health bars in top-right corner
when SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT has active slots; supports
click-to-target and color-coded health bars (red→orange→yellow as HP drops)
- SMSG_RESUME_CAST_BAR: parse packed_guid caster/target + spellId + remainingMs +
totalMs; restores cast bar state when server re-syncs a cast in progress
- SMSG_THREAT_UPDATE: properly consume packed_guid host/target + threat entries
to suppress unhandled packet warnings
- SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT: track up to 5 boss encounter unit guids
per slot; expose via getEncounterUnitGuid(slot); clear on world transfer
These guids identify active boss units for raid/boss frame display.
Previously "Achievement earned! (ID 1234)" was the only message. Now
loadAchievementNameCache() lazily loads Achievement.dbc (field 4 = Title,
verified against WotLK 3.3.5a binary) on first earned event and shows
"Achievement earned: Level 10" or "Player has earned the achievement: ..."
Falls back to ID if DBC is unavailable or entry is missing.
Parse the flight-master POI status packet (guid + uint8 status) and cache
it per-NPC in taxiNpcHasRoutes_. Exposes taxiNpcHasRoutes(guid) accessor
for future nameplate/interaction indicators. Previously this packet was
silently consumed without any state tracking.
Previously all player spell casts played ARCANE school sounds regardless
of the actual spell school. Now loadSpellNameCache() reads SchoolMask
(bitmask, TBC/WotLK) or SchoolEnum (Vanilla/Classic) from Spell.dbc and
stores it in SpellNameEntry. handleSpellStart/handleSpellGo look up the
spell's school and select the correct MagicSchool for cast sounds.
DBC field indices: WotLK SchoolMask=225 (verified), TBC=215, Classic/Turtle
SchoolEnum=1 (Vanilla enum 0-6 converted to bitmask).
Parse SMSG_RESYNC_RUNES, SMSG_ADD_RUNE_POWER, and SMSG_CONVERT_RUNE to
track the state of all 6 DK runes (Blood/Unholy/Frost/Death type,
ready flag, and cooldown fraction). Render a six-square rune bar below
the Runic Power bar when the player is class 6, with per-type colors
(Blood=red, Unholy=green, Frost=blue, Death=purple) and client-side
fill animation so runes visibly refill over the 10s cooldown.
Add PlayPositionalSoundCallback that carries both soundId and sourceGuid.
In Application, look up the source entity position and play via
AudioEngine::playSound3D(); fall back to playSound2D() when the entity
is unknown. Also read the 8-byte sourceGuid field from the packet
(previously the full 12-byte payload was ignored).
Add PlaySoundCallback to GameHandler (same pattern as PlayMusicCallback).
When SMSG_PLAY_SOUND arrives, resolve the soundId through SoundEntries.dbc
(fields 3-12 = files, field 23 = DirectoryBase) and play the first found
file as a 2-D sound effect via AudioEngine::playSound2D(). Previously the
opcode was parsed and dropped.
Add PlayMusicCallback to GameHandler so SMSG_PLAY_MUSIC (and the
vanilla 0x0103 alias) dispatch a soundId to the registered handler
instead of being silently consumed. Application.cpp registers the
callback, loads SoundEntries.dbc, resolves the first non-empty
Name+DirectoryBase into an MPQ path, and passes it to MusicManager
for non-looping playback. Resolves the TODO in the SMSG_PLAY_MUSIC
handler.
- Send CMSG_QUERY_QUESTS_COMPLETED on initial world entry so
completedQuests_ is populated from the server response
- Clear completedQuests_ on world entry to avoid stale data across sessions
- Add isQuestCompleted(questId) and getCompletedQuests() public accessors
to allow UI layers to filter NPC quest offers by completion state