feat(anim): route Commands[] list — full NPC/monster motion support
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>
This commit is contained in:
parent
b7a9322b40
commit
3f41872d88
6 changed files with 341 additions and 11 deletions
|
|
@ -1467,6 +1467,60 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
// No-op if same; the sequencer's fast path guards against that.
|
||||
ae.Sequencer.SetCycle(fullStyle, fullMotion, speedMod);
|
||||
|
||||
// Route the Commands list — one-shot Actions, Modifiers, and
|
||||
// ChatEmotes (mask classes 0x10, 0x20, 0x13 per r03 §3). These
|
||||
// live in the motion table's Links / Modifiers dicts, not
|
||||
// Cycles, and are played on top of the current cycle via
|
||||
// PlayAction which resolves the right dict and interleaves the
|
||||
// action frames before the cyclic tail.
|
||||
//
|
||||
// A typical NPC wave looks like:
|
||||
// ForwardCommand=0 (Ready) + Commands=[{0x0087=Wave, speed=1.0}]
|
||||
// [{0x0003=Ready, ...}]
|
||||
// Each item runs through PlayAction (for 0x10/0x20 mask) or the
|
||||
// standard SetCycle path (for 0x40 SubState). We leave SubState
|
||||
// commands to fall through to the next UpdateMotion; that's how
|
||||
// retail handles transition sequences (Wave → Ready).
|
||||
if (update.MotionState.Commands is { Count: > 0 } cmds)
|
||||
{
|
||||
foreach (var item in cmds)
|
||||
{
|
||||
// Restore the 32-bit MotionCommand from the wire's 16-bit
|
||||
// truncation by OR-ing class bits. The class is encoded
|
||||
// in the low byte's high nibble via command ranges:
|
||||
// 0x0003, 0x0005-0x0010 — SubState class (0x40xx xxxx)
|
||||
// 0x0087 (Wave), 0x007D (BowDeep) etc — ChatEmote (0x13xx xxxx)
|
||||
// 0x0051-0x00A1 — Action class (0x10xx xxxx)
|
||||
//
|
||||
// The retail MotionCommand enum carries the class byte in
|
||||
// bits 24-31. DatReaderWriter's enum values match. For
|
||||
// broadcasts, servers emit only low 16 bits (ACE
|
||||
// InterpretedMotionState.cs:139). We reconstruct via a
|
||||
// range-based lookup. See MotionCommand.generated.cs.
|
||||
uint fullCmd = AcDream.Core.Physics.MotionCommandResolver.ReconstructFullCommand(item.Command);
|
||||
if (fullCmd == 0) continue;
|
||||
|
||||
// Action class: play through the link dict then drop back
|
||||
// to the current cycle. Modifier class: resolve from the
|
||||
// Modifiers dict and combine on top. SubState: cycle
|
||||
// change; route through SetCycle so the style-specific
|
||||
// cycle fallback applies.
|
||||
uint cls = fullCmd & 0xFF000000u;
|
||||
if ((cls & 0x10000000u) != 0 || (cls & 0x20000000u) != 0
|
||||
|| cls == 0x12000000u || cls == 0x13000000u)
|
||||
{
|
||||
ae.Sequencer.PlayAction(fullCmd, item.Speed);
|
||||
}
|
||||
else if ((cls & 0x40000000u) != 0)
|
||||
{
|
||||
// Substate in the command list — typically the "and
|
||||
// then return to Ready" item. Update the cycle.
|
||||
ae.Sequencer.SetCycle(fullStyle, fullCmd, item.Speed);
|
||||
}
|
||||
// else: Style / UI / Toggle class — not animation-driving.
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue