UpdateMotion's InterpretedMotionState payload includes not just
ForwardCommand but a whole Commands[] list of MotionItem entries — each
carrying an Action (attack, portal, skill use), Modifier (jump,
stop-turn), or ChatEmote (Wave, BowDeep, Laugh) that should overlay the
current cycle. The old parser stopped reading after ForwardSpeed, so
emotes/attacks/deaths never reached the sequencer and NPCs just sat in
their idle cycle.
Three parts:
1. New MotionItem wire record in ServerMotionState — carries Command
(u16), PackedSequence (u16 with IsAutonomous bit + 15-bit stamp),
and Speed (f32). Mirrors ACE Network/Motion/MotionItem.cs.
2. Both UpdateMotion.TryParse and CreateObject.TryParseMovementData
now read the full InterpretedMotionState: all 7 flag fields
(CurrentStyle, ForwardCommand, SidestepCommand, TurnCommand,
ForwardSpeed, SidestepSpeed, TurnSpeed) plus the numCommands ×
MotionItem tail. The packed u32 encodes flags in low 7 bits and
command count in bits 7+ (see ACE InterpretedMotionState.cs:131).
3. New MotionCommandResolver — reconstructs the 32-bit MotionCommand
class byte from a 16-bit wire value via a reflection-built lookup
of DatReaderWriter.Enums.MotionCommand. Server serializes as u16
(ACE InterpretedMotionState.cs:139) and we need the class to route:
- 0x10xxxxxx Action / 0x20xxxxxx Modifier / 0x12,0x13 ChatEmote →
PlayAction (resolves from Modifiers or Links dict, overlays on
current cycle)
- 0x40xxxxxx SubState → SetCycle (cycle change)
4. OnLiveMotionUpdated in GameWindow dispatches each command:
- SubState class (0x40xxx) → SetCycle (treated same as
ForwardCommand)
- Action/Modifier/ChatEmote → PlayAction — the link animation
plays once then drops back to the current cycle naturally
(matches retail's action-queue pattern in CMotionInterp
DoInterpretedMotion, decompile FUN_00528F70).
Result: NPCs now animate attacks, waves, bows, death throes, and other
one-shots that ACE broadcasts via the Commands list rather than the
primary ForwardCommand field. Combined with the dead-reckoning + speed-
scaling from the prior commits, remote characters look visually correct
during the full motion spectrum (idle → walk → run → attack → death).
Tests: 2 new UpdateMotion wire-format tests (ForwardSpeed parse, full
Wave command list parse) + 19 new MotionCommandResolver reconstruction
tests covering SubState, Action, and ChatEmote classes. 654 tests green
(was 633).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous tests pinned the buggy 8-byte header assumption. Now they
match ACE's actual wire format: u16 movSeq + u16 srvSeq + u8 isAuto +
1 pad (via BinaryWriter.Align based on absolute stream length 15→16).
All 166 Core.Net tests now green again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server sends UpdateMotion whenever an entity's motion state changes:
NPCs starting a walk cycle, creatures switching to a combat stance,
doors opening, a player waving, etc. Phase 6.1-6.4 already handles
rendering different (stance, forward-command) pairs for the INITIAL
CreateObject, but without this message NPCs freeze in whatever pose
they spawned with and never transition to walking/fighting.
Added UpdateMotion.TryParse with the same ServerMotionState the
CreateObject path uses, reached via a slightly different outer
layout (guid + instance seq + header'd MovementData; the MovementData
starts with the 8-byte sequence/autonomous header this time rather
than being preceded by a length field). Only the (stance, forward-
command) pair is extracted — same subset CreateObject grabs.
WorldSession dispatches MotionUpdated(guid, state) when a 0xF74C
body parses successfully. The App-side wiring (guid→entity lookup
and AnimatedEntity cycle swap) is intentionally deferred to a
separate commit because it touches GameWindow which is currently
being edited by the Phase 9.1 translucent-pass work.
89 Core.Net tests (was 83, +6 for UpdateMotion coverage).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>