fix(anim): Phase L.1b route motion commands

This commit is contained in:
Erik 2026-04-28 10:46:22 +02:00
parent 1c69670392
commit 460f95cb42
6 changed files with 218 additions and 53 deletions

View file

@ -2435,57 +2435,20 @@ public sealed class GameWindow : IDisposable
dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds;
}
// 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).
// Route command-list entries through the shared Core router.
// Retail/ACE send these as 16-bit MotionCommand lows in
// InterpretedMotionState.Commands[]; the router reconstructs the
// class byte and chooses PlayAction for actions/modifiers/emotes
// or SetCycle for persistent substates.
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.
AcDream.Core.Physics.AnimationCommandRouter.RouteWireCommand(
ae.Sequencer,
fullStyle,
item.Command,
item.Speed);
}
}
return;