MSG_MOVE_START_PITCH_UP, MSG_MOVE_START_PITCH_DOWN, MSG_MOVE_STOP_PITCH,
MSG_MOVE_START_ASCEND, MSG_MOVE_STOP_ASCEND were defined in the opcode
table but never routed. The server relays these when another player
pitches or ascends/descends while flying. Without dispatch, position
updates embedded in these packets were silently dropped, causing flying
players to appear to not move vertically. Also adds these to the
compressed-moves opcode recognition array.
handleCompressedMoves uses a hardcoded opcode array to recognise which
sub-packets should be routed to handleOtherPlayerMovement. The two newly
dispatched opcodes were not in this list, so walk/run mode transitions
embedded in SMSG_COMPRESSED_MOVES / SMSG_MULTIPLE_MOVES batches were
silently dropped.
These two opcodes were defined in the opcode table but never routed to
handleOtherPlayerMovement. The server sends them when another player
explicitly toggles walk/run mode. Without dispatch, the WALKING flag
from these packets was never processed, so other players appeared to
always run even after switching to walk mode (until the next heartbeat).
Extends the cold-join fix (block.moveFlags) to the Classic and TBC
parseMovementBlock implementations so that SMSG_UPDATE_OBJECT CREATE
packets on Classic/TBC servers also initialise entity swim/walk state
from the spawn-time movement flags via unitMoveFlagsCallback_.
The previous fix added SWIMMING (0x00200000) correctly but kept 0x02000000
which is SPLINE_ELEVATION (smooth vertical spline offset), not the FLYING
flag. WotLK 3.3.5a FLYING = 0x01000000; pitch should be read when SWIMMING
or FLYING are active. This corrects the condition and updates the comment.
UpdateBlock now stores moveFlags from the LIVING movement block so the
cold-join problem is fixed: entities already swimming or walking when the
client joins get their animation state correctly initialised from the
SMSG_UPDATE_OBJECT CREATE_OBJECT packet rather than waiting for the next
MSG_MOVE_* heartbeat.
Additionally, SMSG_SPLINE_MOVE_START_SWIM, SMSG_SPLINE_MOVE_STOP_SWIM,
SMSG_SPLINE_MOVE_SET_WALK_MODE, SMSG_SPLINE_MOVE_SET_RUN_MODE, and
SMSG_SPLINE_MOVE_SET_FLYING now fire unitMoveFlagsCallback_ with
synthesised flags so explicit server-driven mode transitions update
animation state immediately without waiting for a heartbeat.
parseMovementBlock was checking moveFlags & 0x02000000 for the pitch
field, but SWIMMING is 0x00200000 in WotLK 3.3.5a. Swimming NPCs and
players in SMSG_UPDATE_OBJECT packets never triggered the pitch read,
so the fallTime/jumpData/splineElevation fields were read from the wrong
offsets, producing incorrect positions and orientations.
Fix: check both SWIMMING (0x00200000) and FLYING (0x02000000), matching
the WotLK format — same condition used in the write path.
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.
- Add creatureSwimmingState_ map to track which units are swimming
- unitAnimHintCallback with animId=42 (Swim): marks entity as swimming
- unitAnimHintCallback with animId=0 (MSG_MOVE_STOP_SWIM): clears swim state
- Per-frame sync: uses Swim(42)/SwimIdle(41) when swimming, Run(5)/Stand(0) otherwise
— creatures/players now show SwimIdle when standing still in water
- Clear creatureSwimmingState_ on creature/player despawn and world reset
- 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)
The renderer's CharAnimState machine already drives player character
animations (Run=5, Walk=4, Jump, Swim, etc.) — remove the conflicting
camera controller code added in the previous commit.
Fix creature movement animations to use the correct WoW M2 IDs:
4=Walk, 5=Run. Both the per-frame sync loop and the SMSG_MONSTER_MOVE
spline callback now use Run (5) for NPC movement.
CameraController now plays Walk (5) for backpedal/slow pace and Run (4)
for forward running (runPace), matching WoW's animation convention.
Also handles transitions between Walk and Run while already moving.
creatureMoveCallback now plays anim 4 (Run) when a spline move begins
(durationMs > 0), mirroring the per-frame sync logic so NPC/player
characters animate correctly during server-driven path moves as well
as position-sync moves.
CameraController now transitions the player character to Run (anim 4)
on movement start and back to Stand (anim 0) on stop, guarded by a
prevPlayerMoving_ flag so animation time is not reset every frame.
Death animation (anim 1) is never overridden.
Application creature sync similarly switches creature models to Run (4)
when they move between server positions and Stand (0) when they stop,
with per-guid creatureWasMoving_ tracking to avoid per-frame resets.
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 CameraController::setSitting() and call it from the StandStateCallback
so the camera blocks movement when the server confirms the player is
sitting or kneeling (stand states 1-6, 8). This prevents the player
from sliding across the ground after sitting.
Death (state 7) deliberately leaves sitting=false so the player can
still respawn/move after death without input being blocked.
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.
When a non-looping animation (e.g. wave, cheer, laugh emote) reaches
its end, transition back to Stand (animation 0) rather than freezing
on the last frame. Death (animation 1) is excluded — it stays on the
final frame as expected.
Fixes NPCs and players getting stuck in emote poses after SMSG_EMOTE.
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.
SMSG_EMOTE packets for NPCs and other players were received but the
emoteAnimCallback_ was never wired to the rendering layer. Register
the callback in application.cpp so that when the server sends an emote
animation ID, the corresponding CharacterRenderer instance plays it as
a one-shot animation (loop=false), falling back to idle on completion.
Lookups check creatureInstances_ first, then playerInstances_ so both
NPCs and other online players respond to server emote packets.
Previously disabled because the per-frame raycast caused erratic zoom
snapping at doorway transitions. Re-enable using an asymmetrically-
smoothed collision limit: pull-in reacts quickly (τ≈60 ms) to prevent
the camera from ever visibly clipping through walls, while recovery is
slow (τ≈400 ms) so walking through a doorway zooms back out gradually
instead of snapping.
Uses wmoRenderer->raycastBoundingBoxes() which already has strict wall
filters (|normal.z|<0.20, surface-alignment check, ±0.9 height band)
to ignore floors, ramps, and arch geometry.
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.
After reconnect, `creaturePermanentFailureGuids_` and `deadCreatureGuids_`
could retain stale entries for GUIDs not tracked in `creatureInstances_`
(creatures that failed to load or died before being spawned). These stale
entries would silently block re-spawning or cause wrong death state on the
fresh CREATE_OBJECTs the server sends after reconnect.
Clear both caches in the reconnect-to-same-map path so server state is
authoritative after every reconnect.
The previous reconnect fix caused loadOnlineWorldTerrain to run, which
cleared and reloaded all terrain tiles — unnecessarily heavy for a
reconnect where the map hasn't changed.
New path: when isInitialEntry=true and mapId==loadedMapId_, despawn all
tracked creature/player/GO instances from the renderer (proper cleanup),
clear all pending spawn queues, update player position, and return — the
terrain stays loaded and the server's fresh CREATE_OBJECTs repopulate
entities normally.
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
processAsyncNpcCompositeResults() had no per-frame budget cap, so when
many NPCs finished async skin compositing simultaneously (e.g. right
after world load), all results were finalized in a single frame causing
up to 284ms frame stalls. Apply the same 2ms budget pattern used by
processAsyncCreatureResults. Load screen still processes all pending
composites without the cap (unlimited=true).
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.
The selection circle was positioned using the entity's game-logic
interpolator (entity->getX/Y/Z), while the actual M2 model is
positioned by CharacterRenderer's independent interpolator (moveInstanceTo).
These two systems can drift apart during movement, causing the circle
to appear under the wrong position relative to the visible model.
Fix: add CharacterRenderer::getInstancePosition / Application::getRenderPositionForGuid
and use the renderer's inst.position for XY (with footZ override for Z)
so the circle always tracks the rendered model exactly. Falls back to
the entity game-logic position when no CharacterRenderer instance exists.
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.
Draws the current zone name centered above the minimap circle using a
gold-colored 12pt label with drop shadow. This gives players a constant
location reference without needing to trigger the full-screen zone flash.
Uses getForegroundDrawList so it renders above the minimap texture.
Left-clicking anywhere within a nameplate's bounding box (name text +
health bar) calls setTarget() for that unit. Uses manual mouse position
hit testing since nameplates are drawn on the background DrawList rather
than as ImGui widgets. Click is ignored when ImGui has captured the mouse
(e.g. when a window is open).
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
The V toggle previously only rendered a nameplate for the currently
targeted unit. Now all nearby units get a nameplate when nameplates are
enabled, matching WoW's native behaviour:
- Target: nameplate shown up to 40 units, gold border highlight
- All other units: shown up to 20 units, dark border (no highlight)
- Fade-out range, hostility colour, level label, and health bar logic
are all unchanged — only the per-entity distance culling changes
Replace the plain yellow text cooldown overlay with a proper clock-sweep:
- Dark fan spanning the elapsed fraction of the cooldown, sweeping
clockwise from 12 o'clock (matches WoW's native cooldown look)
- White remaining-time text with drop-shadow centered on the icon
- Minutes shown as "Xm" for cooldowns >= 60s, seconds otherwise
- Fan radius set to 1.5× the icon half-width to cover corners on the
square icon; works for both icon and empty (label-only) slots
Draw a dot for each online party member that has reported a position via
SMSG_PARTY_MEMBER_STATS. Leader gets a gold dot, others get blue. A
white outline ring is drawn around each dot, and hovering over it shows
the member's name as a tooltip. Out-of-range members are silently
skipped by the existing projectToMinimap clamp logic.
Axis mapping follows the same convention as minimap pings: server posX
(east/west) → canonical Y, server posY (north/south) → canonical X.