feat(R2-Q4): adapter cutover — SetCycle/PlayAction rehosted on PerformMovement; Fix B / fast-path / stop-anim fallback / G17 gate DELETED

AnimationSequencer becomes a thin shim over the verbatim R2 stack:
SetCycle dispatches the style change (Branch 1) then the motion (Branch
2/3/4) through MotionTableManager.PerformMovement; PlayAction is the
same dispatch (actions rebuild via Branch 3; modifiers are Branch 4
physics-only combine_motion — the AP-73 turn-blend mechanism now runs
for real). CurrentStyle/CurrentMotion/CurrentSpeedMod are read-only
mirrors of MotionState (post-adjust_motion, signed mod). Lazy
initialize_state on first drive (retail lazy-create analog) keeps
undriven sequencers do-nothing.

DELETED legacy inventions (net -648 lines): the adapter fast-path
(replaced by Branch-2 fast re-speed: change_cycle_speed +
subtract/combine_motion), Fix B + IsLocomotionCycleLowByte (replaced by
remove_redundant_links on the pending queue — the retail mechanism the
2026-05-03 cdb trace pointed at), the stop-anim low-byte fallback
(replaced by get_link's reversed-key double-hop — retail's actual
backward-walk settle plays the windup link REVERSED), GetLink/BuildNode/
EnqueueMotionData + the G17 HasVelocity/HasOmega gate (add_motion sets
unconditionally), MultiplyCyclicFramerate + the G13 composite,
PlayAction's insert-before-tail machinery, SCFAST/SCFULL diagnostics.
KEPT at the adapter (register rows): the boundary adjust_motion remap +
locomotion velocity/omega synthesis (AP-75, retire R3-W6/R6), K-fix18
skip-link as post-dispatch RemoveAllLinkAnimations (AP-74, retire
R3-W4 when LeaveGround fires it).

GameWindow queue-drain wiring (the §4 G6 seam): each drained AnimDone
hook → manager.AnimationDone(true) (retail AnimDoneHook::Execute
0x00526c20 → Hook_AnimDone 0x0050fda0 chain), UseTime once per tick
(0x00517d57/0x00517d67); IMotionDoneSink bound to an
ACDREAM_DUMP_MOTION recorder (AD-36 — R3 rebinds to
MotionInterpreter.MotionDone).

Conformance: the 11-scenario pre-cutover trace suite (a6235a36) replays
green with six EXPECTED-DIFF annotations (double-hop walk-run routing,
reversed settle links, Branch-4 physics-only modifiers, Branch-1
style-change links, post-adjust mirrors) — everything unannotated is
byte-identical. Legacy suites repaired by a dedicated agent (fixtures
gain the retail-mandatory StyleDefaults + class-bit-tagged ids;
reflection state-poking replaced with real dispatch; zero category-D
regressions — the suspected cursor bug was a fixture class-bit gap),
plus two vacuously-passing tests fixed. Register: stale IA-4 deleted
(R1-P1 ported the negative-factor swap).

Full suite green: 3,458 passed (374+425+713+1946).

Closes H6, H7, H8, H9, H10-adapter, H16-wiring (r2-port-plan.md §3 Q4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 21:41:27 +02:00
parent cd0289bea2
commit 3b9d9bb6be
6 changed files with 562 additions and 646 deletions

View file

@ -1,5 +1,6 @@
using AcDream.Core.Physics;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
@ -24,7 +25,13 @@ public sealed class AnimationCommandRouterTests
[Fact]
public void RouteWireCommand_SubState_UsesSetCycle()
{
var seq = MakeEmptySequencer();
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
// — GetObjectSequence also refuses substate==0, so the MotionTable
// needs both a StyleDefaults entry AND cycles for the style's default
// substate (Ready, installed by initialize_state) and for the routed
// Dead substate, or the whole dispatch chain silently no-ops and
// CurrentStyle/CurrentMotion stay at their zero defaults.
var seq = MakeRoutableSequencer();
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0011);
@ -59,8 +66,66 @@ public sealed class AnimationCommandRouterTests
return new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
}
/// <summary>
/// R2-Q4: a MotionTable that can actually complete a SubState dispatch —
/// StyleDefaults[NonCombat]=Ready (required by initialize_state's
/// SetDefaultState) plus cycles for both Ready (the installed baseline)
/// and Dead (the substate this test routes to). One shared part/anim is
/// enough; RouteWireCommand only inspects CurrentStyle/CurrentMotion.
/// </summary>
private static AnimationSequencer MakeRoutableSequencer()
{
const uint Ready = 0x41000003u;
const uint Dead = 0x40000011u;
const uint AnimId = 0x03000001u;
var setup = new Setup();
setup.Parts.Add(0x01000000u);
setup.DefaultScale.Add(System.Numerics.Vector3.One);
var mt = new MotionTable
{
DefaultStyle = (DatReaderWriter.Enums.MotionCommand)NonCombat,
};
mt.StyleDefaults[(DatReaderWriter.Enums.MotionCommand)NonCombat] =
(DatReaderWriter.Enums.MotionCommand)Ready;
int ReadyKey = (int)((NonCombat << 16) | (Ready & 0xFFFFFFu));
int DeadKey = (int)((NonCombat << 16) | (Dead & 0xFFFFFFu));
mt.Cycles[ReadyKey] = MakeMotionData(AnimId);
mt.Cycles[DeadKey] = MakeMotionData(AnimId);
var loader = new NullAnimationLoader();
loader.Register(AnimId, MakeAnim());
return new AnimationSequencer(setup, mt, loader);
}
private static MotionData MakeMotionData(uint animId)
{
var md = new MotionData();
QualifiedDataId<Animation> qid = animId;
md.Anims.Add(new AnimData { AnimId = qid, LowFrame = 0, HighFrame = -1, Framerate = 30f });
return md;
}
private static Animation MakeAnim()
{
var anim = new Animation();
var pf = new AnimationFrame(1);
pf.Frames.Add(new Frame
{
Origin = System.Numerics.Vector3.Zero,
Orientation = System.Numerics.Quaternion.Identity,
});
anim.PartFrames.Add(pf);
return anim;
}
private sealed class NullAnimationLoader : IAnimationLoader
{
public Animation? LoadAnimation(uint id) => null;
private readonly System.Collections.Generic.Dictionary<uint, Animation> _anims = new();
public void Register(uint id, Animation anim) => _anims[id] = anim;
public Animation? LoadAnimation(uint id) => _anims.TryGetValue(id, out var a) ? a : null;
}
}

View file

@ -254,8 +254,16 @@ public sealed class AnimationSequencerCutoverTraceTests
seq.SetCycle(NC, Ready);
seq.SetCycle(NC, Walk, 1.0f);
seq.SetCycle(NC, Run, 2.0f);
// EXPECTED-DIFF(Q4): pre-cutover Fix B stripped every link leaving
// "102@60.0*^". The verbatim GetObjectSequence (Branch 2, no direct
// Walk->Run link in this fixture) routes the DOUBLE-HOP: Walk->Ready
// settle (105 at the OLD substate mod) + Ready->Run windup (104 at
// the new speed) + the Run cycle; the old Ready->Walk link (103)
// keeps draining first (pending-queue discipline). Rapid same-motion
// re-issues now collapse via remove_redundant_links (the retail
// Fix B), not an adapter locomotion special case.
Assert.Equal(
"102@60.0*^ | frame=0.0 vel=(0.00,8.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.00",
"103@30.0^,105@30.0,104@60.0,102@60.0* | frame=0.0 vel=(0.00,8.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.00",
Describe(seq, loader));
}
@ -267,8 +275,12 @@ public sealed class AnimationSequencerCutoverTraceTests
seq.SetCycle(NC, Walk, 1.0f);
seq.SetCycle(NC, Run, 2.0f);
seq.SetCycle(NC, Run, 2.5f);
// EXPECTED-DIFF(Q4): inherits S3's double-hop list; the re-speed
// itself is the verbatim Branch-2 fast path (change_cycle_speed
// scales ONLY first_cyclic..tail: 102 60->75; links untouched;
// velocity via subtract_motion(2.0)+combine_motion(2.5)).
Assert.Equal(
"102@75.0*^ | frame=0.0 vel=(0.00,10.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.50",
"103@30.0^,105@30.0,104@60.0,102@75.0* | frame=0.0 vel=(0.00,10.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.50",
Describe(seq, loader));
}
@ -278,11 +290,18 @@ public sealed class AnimationSequencerCutoverTraceTests
var seq = NewSeq(out var loader);
seq.SetCycle(NC, Ready);
seq.SetCycle(NC, WalkBack, 1.0f);
// Reversed-key GetLink resolves the Walk→Ready link (0x105) played in
// reverse (fr=-19.5 = 30 × -0.65 BackwardsFactor); cursor starts at
// the link's reverse starting frame (HighFrame+1 = 3).
// Node list BYTE-IDENTICAL to pre-cutover: the reversed-key get_link
// resolves the Walk->Ready link (0x105) played in reverse (fr=-19.5 =
// 30 x -0.65 BackwardsFactor); cursor at the reverse starting frame
// (HighFrame+1 = 3).
// EXPECTED-DIFF(Q4), mirrors only: CurrentMotion now reads the
// POST-adjust_motion substate (45000005, was 45000006) and
// CurrentSpeedMod the signed mod (-0.65, was 1.00) - MotionState owns
// the state and retail's interpreted state is post-adjustment. The
// om=(-0.00,...) is IEEE negative zero from add_motion's
// zero-omega x negative-speed multiply (numerically equal to 0).
Assert.Equal(
"105@-19.5^,101@-19.5* | frame=3.0 vel=(0.00,-2.03,0.00) om=(0.00,0.00,0.00) style=8000003D motion=45000006 mod=1.00",
"105@-19.5^,101@-19.5* | frame=3.0 vel=(0.00,-2.03,0.00) om=(-0.00,-0.00,-0.00) style=8000003D motion=45000005 mod=-0.65",
Describe(seq, loader));
}
@ -293,12 +312,14 @@ public sealed class AnimationSequencerCutoverTraceTests
seq.SetCycle(NC, Ready);
seq.SetCycle(NC, WalkBack, 1.0f);
seq.SetCycle(NC, Ready, 1.0f);
// Pre-cutover: the OLD reversed link (0x105@-19.5, mid-drain) stays at
// the head while the settle link (0x105@30, via the stop-anim
// low-byte fallback 0x06→0x05) and the Ready cycle append behind it —
// link-before-link stacking.
// EXPECTED-DIFF(Q4): pre-cutover the stop-anim low-byte fallback
// re-keyed the settle as 105@30 (forward). The verbatim get_link
// (SubstateMod=-0.65 routes the REVERSED-key branch) resolves the
// Ready->Walk windup (103) played in REVERSE (fr=-30) - retail's
// actual backward-walk settle. The old reversed link (105@-19.5,
// mid-drain) still drains first, unchanged from pre-cutover.
Assert.Equal(
"105@-19.5^,105@30.0,100@30.0* | frame=3.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=41000003 mod=1.00",
"105@-19.5^,103@-30.0,100@30.0* | frame=3.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=41000003 mod=1.00",
Describe(seq, loader));
}
@ -332,8 +353,13 @@ public sealed class AnimationSequencerCutoverTraceTests
seq.SetCycle(NC, Ready);
seq.SetCycle(NC, Walk, 1.0f);
seq.PlayAction(TurnMod, 1.0f);
// EXPECTED-DIFF(Q4): pre-cutover PlayAction INSERTED the modifier's
// anim (109) before the cyclic tail - an acdream invention. Retail
// Branch 4 is PHYSICS-ONLY combine_motion (the AP-73 mechanism): the
// walk cycle's frames are untouched, the modifier contributes omega
// (0,0,1.5) and is tracked on the MotionState modifier stack.
Assert.Equal(
"103@30.0^,109@30.0,101@30.0* | frame=0.0 vel=(0.00,3.12,0.00) om=(0.00,0.00,1.50) style=8000003D motion=45000005 mod=1.00",
"103@30.0^,101@30.0* | frame=0.0 vel=(0.00,3.12,0.00) om=(0.00,0.00,1.50) style=8000003D motion=45000005 mod=1.00",
Describe(seq, loader));
}
@ -343,8 +369,12 @@ public sealed class AnimationSequencerCutoverTraceTests
var seq = NewSeq(out var loader);
seq.SetCycle(NC, Ready);
seq.SetCycle(Style2, Ready, 1.0f);
// EXPECTED-DIFF(Q4): pre-cutover switched cycles bare ("107@30.0*^").
// GetObjectSequence Branch 1 (style-change) plays the cross-style
// entry link (10A = Ready->Style2) before the target style's default
// cycle - retail's stance-transition animation.
Assert.Equal(
"107@30.0*^ | frame=0.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000004C motion=41000003 mod=1.00",
"10A@30.0^,107@30.0* | frame=0.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000004C motion=41000003 mod=1.00",
Describe(seq, loader));
}
}

View file

@ -333,13 +333,21 @@ public sealed class AnimationSequencerTests
[Fact]
public void SetCycle_WithTransitionLink_PrependLinkFrames()
{
// Two animations: link (2 frames at Y=1) and cycle (4 frames at X=1).
const uint Style = 0x003Du;
const uint IdleMotion = 0x0003u;
const uint WalkMotion = 0x0005u;
// R2-Q4: GetObjectSequence gates Branch 2 (cyclic substates) on the
// 0x40000000 class bit and Branch 1 (style change) on the top bit —
// bare low-word ids like the pre-cutover 0x0003/0x0005 never satisfy
// those gates and the dispatch silently fails. Tag Style/IdleMotion/
// WalkMotion with their class bits (masking to the low 24 bits for
// the cycle/link key hash is unaffected — CMotionTable keys on
// `id & 0xFFFFFF`).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x40000003u;
const uint WalkMotion = 0x40000005u;
const uint IdleAnim = 0x03000012u;
const uint CycleAnim = 0x03000010u;
const uint LinkAnim = 0x03000011u;
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
var cycleAnim = Fixtures.MakeAnim(4, 1, new Vector3(1, 0, 0), Quaternion.Identity);
var linkAnim = Fixtures.MakeAnim(2, 1, new Vector3(0, 1, 0), Quaternion.Identity);
@ -352,15 +360,26 @@ public sealed class AnimationSequencerTests
fromMotion: IdleMotion,
toMotion: WalkMotion,
linkAnimId: LinkAnim);
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0) —
// route it at IdleMotion (the state this test "was already playing"
// before priming) and give IdleMotion its own cycle so the real
// SetCycle(Style, IdleMotion) priming call below actually dispatches.
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 30f);
var loader = new FakeLoader();
loader.Register(IdleAnim, idleAnim);
loader.Register(CycleAnim, cycleAnim);
loader.Register(LinkAnim, linkAnim);
var seq = new AnimationSequencer(setup, mt, loader);
// Prime the sequencer as if it was already playing IdleMotion.
SetCurrentMotion(seq, Style, IdleMotion);
// Prime the sequencer as if it was already playing IdleMotion — a
// real SetCycle call now that CurrentStyle/CurrentMotion are
// read-only mirrors of MotionState (R2-Q4; reflection SetValue no
// longer works, "Property set method not found").
seq.SetCycle(Style, IdleMotion);
seq.SetCycle(Style, WalkMotion);
@ -382,9 +401,13 @@ public sealed class AnimationSequencerTests
// link's starting pose at the link→cycle boundary. Symptoms: door
// swing-open flap (frame 0 = closed); player run-stop twitch
// (frame 0 = mid-stride).
const uint Style = 0x003Du;
const uint IdleMotion = 0x0003u;
const uint WalkMotion = 0x0005u;
// R2-Q4: class-bit-tagged ids (GetObjectSequence gates Branch 1/2 on
// the 0x80000000/0x40000000 bits) — masking to the low 24 bits for
// the cycle/link key hash is unaffected.
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x40000003u;
const uint WalkMotion = 0x40000005u;
const uint IdleAnim = 0x03000082u;
const uint CycleAnim = 0x03000080u;
const uint LinkAnim = 0x03000081u;
@ -402,6 +425,7 @@ public sealed class AnimationSequencerTests
// Cycle anim: single frame at Y=0 (the "open" / "idle" rest pose).
var cycleAnim = Fixtures.MakeAnim(1, 1, new Vector3(0, 0, 0), Quaternion.Identity);
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = Fixtures.MakeMtable(
@ -412,13 +436,23 @@ public sealed class AnimationSequencerTests
toMotion: WalkMotion,
linkAnimId: LinkAnim,
framerate: 30f);
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
// — route it at IdleMotion (the state we prime through below) with
// its own cycle so the priming SetCycle call actually dispatches.
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 30f);
var loader = new FakeLoader();
loader.Register(IdleAnim, idleAnim);
loader.Register(CycleAnim, cycleAnim);
loader.Register(LinkAnim, linkAnim);
var seq = new AnimationSequencer(setup, mt, loader);
SetCurrentMotion(seq, Style, IdleMotion);
// Prime the sequencer as if it was already playing IdleMotion — a
// real SetCycle call (reflection SetValue no longer works against
// the now-read-only CurrentStyle/CurrentMotion mirrors).
seq.SetCycle(Style, IdleMotion);
seq.SetCycle(Style, WalkMotion);
// Advance to _framePosition ≈ 2.5 — past the last integer frame (2)
@ -440,53 +474,71 @@ public sealed class AnimationSequencerTests
[Fact]
public void SetCycle_StopFromWalkBackward_FallsBackToWalkForwardStopLink()
{
// Stop-anim asymmetry: the Humanoid motion table only authors a
// "stop walking" link under WalkForward (low byte 0x05). Stopping
// from WalkBackward (0x06) without a fallback returns null linkData
// and the cycle snaps to Ready with no settle blend. Fix: when the
// primary GetLink lookup fails, retry with WalkBackward's low byte
// remapped to WalkForward.
const uint Style = 0x003Du;
const uint WalkForwardCmd = 0x0005u;
const uint WalkBackCmd = 0x0006u;
const uint ReadyCmd = 0x0003u;
// R2-Q4 EXPECTED-DIFF (mirrors AnimationSequencerCutoverTraceTests.
// S6_WalkBackToReady_StopSettleFallback): the pre-cutover adapter had
// an invented "stop-anim low-byte fallback" that re-keyed a
// WalkForward->Ready link when a WalkBackward->Ready lookup missed.
// Retail has no such fallback — CMotionTable.GetLink's verbatim
// reversed-key branch (Q0-pins A1) does the real job: adjust_motion
// remaps WalkBackward to WalkForward with a NEGATIVE SubstateMod, so
// stopping from it drives GetLink's "either speed negative -> swapped
// keys" path, which resolves the link stored FROM the style default
// (Ready) TO WalkForward and plays it IN REVERSE (the Ready->Walk
// windup run backward as a settle). The fixture below stores that
// link under Links[(style,Ready)][WalkForward] — the opposite
// direction from the old WalkForward->Ready fallback entry — because
// that's the key GetLink's reversed branch actually probes.
const uint Style = 0x8000003Du;
const uint WalkForwardCmd = 0x40000005u;
const uint WalkBackCmd = 0x40000006u;
const uint ReadyCmd = 0x40000003u;
const uint CycleAnim = 0x03000090u; // Ready cycle (Y=0)
const uint LinkAnim = 0x03000091u; // stop-link (Y=7)
const uint LinkAnim = 0x03000091u; // Ready->Walk windup (Y=7), played reversed as the settle
var cycleAnim = Fixtures.MakeAnim(1, 1, new Vector3(0, 0, 0), Quaternion.Identity);
var linkAnim = Fixtures.MakeAnim(4, 1, new Vector3(0, 7, 0), Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
// Table: Ready cycle + WalkForward→Ready link. NO WalkBackward→Ready link.
// Table: Ready cycle + Ready->WalkForward windup link (probed
// REVERSED by GetLink's swapped-key branch when settling out of a
// negative-SubstateMod substate). No forward WalkBackward cycle is
// needed — adjust_motion remaps WalkBackward to WalkForward with a
// negated + BackwardsFactor-scaled speed before dispatch ever sees it.
var mt = Fixtures.MakeMtable(
style: Style,
motion: ReadyCmd,
cycleAnimId: CycleAnim,
fromMotion: WalkForwardCmd,
toMotion: ReadyCmd,
fromMotion: ReadyCmd,
toMotion: WalkForwardCmd,
linkAnimId: LinkAnim,
framerate: 30f);
// WalkForward also needs a cycle — adjust_motion's WalkBackward remap
// dispatches WalkForward's cycle (with the negated/scaled speed) as
// part of entering "backward" motion below.
int walkKey = (int)((Style << 16) | (WalkForwardCmd & 0xFFFFFFu));
mt.Cycles[walkKey] = Fixtures.MakeMotionData(CycleAnim, framerate: 30f);
var loader = new FakeLoader();
loader.Register(CycleAnim, cycleAnim);
loader.Register(LinkAnim, linkAnim);
var seq = new AnimationSequencer(setup, mt, loader);
// Simulate "we were walking backward" — substate = WalkBackward,
// substateSpeed = +1 (the original speedMod stored by SetCycle).
SetCurrentMotion(seq, Style, WalkBackCmd);
// Enter WalkBackward for real — SetCycle's adjust_motion head remaps
// this to WalkForward with SubstateMod = -0.65 (BackwardsFactor),
// which is what makes the SUBSEQUENT stop-to-Ready call route
// GetLink's reversed-key branch.
seq.SetCycle(Style, WalkBackCmd, 1.0f);
seq.SetCycle(Style, ReadyCmd);
// Advance a tiny dt — should land on link frame 0 (Y=7), not the
// cycle (Y=0). Without the fallback, linkData is null, only the
// Ready cycle is enqueued, and we read Y=0 immediately.
// Advance a tiny dt — should land on the reversed windup link
// (Y=7), not the Ready cycle (Y=0).
var transforms = seq.Advance(0.001f);
Assert.Single(transforms);
Assert.True(transforms[0].Origin.Y > 5f,
$"Stop-from-backward should fall back to WalkForward→Ready link "
+ $"(expect Y≈7 from link); got Y={transforms[0].Origin.Y} "
+ "(Y=0 means linkData was null and we snapped to Ready cycle).");
$"Stop-from-backward should resolve GetLink's reversed-key branch "
+ $"(expect Y≈7 from the reversed windup link); got Y={transforms[0].Origin.Y} "
+ "(Y=0 means the link didn't resolve and we snapped to the Ready cycle).");
}
[Fact]
@ -581,10 +633,14 @@ public sealed class AnimationSequencerTests
// with negated speed, so the animation plays in reverse.
// We verify this by checking CurrentMotion is still TurnLeft (the
// original command), but the sequencer internally uses TurnRight's anim.
// R2-Q4: Style needs the 0x80000000 top bit and TurnRight/TurnLeft the
// 0x40000000 cycle-class bit — GetObjectSequence's entry/branch gates
// (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity) test the
// FULL command word, not just the low 16 bits adjust_motion remaps.
const uint Style = 0x003Du; // NonCombat
const uint TurnRight = 0x0045000Du; // bit pattern for TurnRight in NonCombat
const uint TurnLeft = 0x0045000Eu; // bit pattern for TurnLeft
const uint Style = 0x8000003Du; // NonCombat
const uint TurnRight = 0x4045000Du; // bit pattern for TurnRight in NonCombat
const uint TurnLeft = 0x4045000Eu; // bit pattern for TurnLeft
const uint AnimId = 0x03000050u;
// 4-frame animation; each frame has a distinct Z-origin so we can tell
@ -600,6 +656,11 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
// — route the default straight at TurnRight (the only cycle this
// fixture defines) so initialize_state's baseline install succeeds
// and state.Style/Substate are non-zero before the explicit dispatch.
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)TurnRight;
// Register TurnRight cycle (adjusted motion, not TurnLeft).
int cycleKey = (int)((Style << 16) | (TurnRight & 0xFFFFFFu));
@ -611,8 +672,16 @@ public sealed class AnimationSequencerTests
var seq = new AnimationSequencer(setup, mt, loader);
seq.SetCycle(Style, TurnLeft, speedMod: 1f);
// CurrentMotion should record the original TurnLeft command.
Assert.Equal(TurnLeft, seq.CurrentMotion);
// R2-Q4 EXPECTED-DIFF (mirrors AnimationSequencerCutoverTraceTests.
// S5_WalkBackward_RemapNegativeSpeed's "mirrors only" diff):
// CurrentMotion is now a GET-ONLY mirror of MotionState.Substate,
// and MotionState owns the POST-adjust_motion substate — retail's
// interpreted state IS the adjusted one (TurnRight played reversed),
// not the raw TurnLeft the caller passed in. Pre-cutover the adapter
// kept its own separate CurrentMotion field and never overwrote it
// with the adjusted id; that field no longer exists.
Assert.Equal(TurnRight, seq.CurrentMotion);
Assert.Equal(-1f, seq.CurrentSpeedMod, 3);
// R1-P5 (2026-07-02): pre-cutover this pinned the ACE-fabricated
// epsilon boundary ((EndFrame+1)-eps ~= 3.99999). Retail's
@ -633,14 +702,17 @@ public sealed class AnimationSequencerTests
public void Advance_NegativeSpeed_FramePositionDecreases()
{
// Verify that a cycle loaded with negative framerate counts downward.
const uint Style = 0x003Du;
const uint Motion = 0x0003u;
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000003u;
const uint AnimId = 0x03000060u;
var anim = Fixtures.MakeAnim(8, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
// Register cycle with NEGATIVE framerate to simulate reverse playback.
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
@ -726,9 +798,11 @@ public sealed class AnimationSequencerTests
{
// Queue: [linkNode (2 frames, 10fps, non-looping)] → [cycleNode (4 frames, looping)]
// Advance enough to exhaust the link node, then verify we're in the cycle.
const uint Style = 0x003Du;
const uint IdleMotion = 0x0003u;
const uint WalkMotion = 0x0005u;
// R2-Q4: class-bit-tagged ids (see SetCycle_WithTransitionLink_PrependLinkFrames).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x40000003u;
const uint WalkMotion = 0x40000005u;
const uint IdleAnim = 0x03000082u;
const uint CycleAnim = 0x03000080u;
const uint LinkAnim = 0x03000081u;
@ -736,6 +810,7 @@ public sealed class AnimationSequencerTests
var linkAnim = Fixtures.MakeAnim(2, 1, new Vector3(0, 5, 0), Quaternion.Identity);
// Cycle anim: 4 frames, X=9 (distinct marker).
var cycleAnim = Fixtures.MakeAnim(4, 1, new Vector3(9, 0, 0), Quaternion.Identity);
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = Fixtures.MakeMtable(
@ -746,13 +821,22 @@ public sealed class AnimationSequencerTests
toMotion: WalkMotion,
linkAnimId: LinkAnim,
framerate: 10f);
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
// — route it at IdleMotion with its own cycle so the priming
// SetCycle call below actually dispatches.
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 10f);
var loader = new FakeLoader();
loader.Register(IdleAnim, idleAnim);
loader.Register(CycleAnim, cycleAnim);
loader.Register(LinkAnim, linkAnim);
var seq = new AnimationSequencer(setup, mt, loader);
SetCurrentMotion(seq, Style, IdleMotion);
// Prime as if already playing IdleMotion — real SetCycle call
// (reflection SetValue no longer works, see WithTransitionLink test).
seq.SetCycle(Style, IdleMotion);
seq.SetCycle(Style, WalkMotion);
// Link node is 2 frames at 10fps → 0.2s to exhaust.
@ -923,8 +1007,11 @@ public sealed class AnimationSequencerTests
public void Advance_ForwardHookDoesNotFire_OnReversePlayback()
{
// A hook tagged Direction.Forward should NOT fire when playback is reversed.
const uint Style = 0x003Du;
const uint Motion = 0x0003u;
// R2-Q4: class-bit-tagged ids + retail-mandatory StyleDefaults — the bare
// ids made this test pass VACUOUSLY (dispatch silently failed, no anim
// played, so "hook did not fire" held for the wrong reason).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000003u;
const uint AnimId = 0x03000103u;
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
@ -937,6 +1024,7 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
var md = new MotionData();
QualifiedDataId<Animation> qid = AnimId;
@ -951,7 +1039,10 @@ public sealed class AnimationSequencerTests
seq.ConsumePendingHooks();
// Reverse playback: cursor starts near frame 4 and counts down.
seq.Advance(0.15f);
// 0.25s at -10fps = -2.5 frames → crosses the 3→2 boundary, so the
// hooked frame IS reached (same advance as the Backward sibling test
// — the direction filter, not distance, is what's under test).
seq.Advance(0.25f);
var hooks = seq.ConsumePendingHooks();
// Forward-only hook on frame 2 should NOT fire on reverse playback.
@ -961,8 +1052,10 @@ public sealed class AnimationSequencerTests
[Fact]
public void Advance_BackwardHook_FiresOnReversePlayback()
{
const uint Style = 0x003Du;
const uint Motion = 0x0003u;
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000003u;
const uint AnimId = 0x03000104u;
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
@ -975,6 +1068,7 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
var md = new MotionData();
QualifiedDataId<Animation> qid = AnimId;
@ -1045,8 +1139,13 @@ public sealed class AnimationSequencerTests
public void CurrentVelocity_ExposedFromMotionData_WhenHasVelocity()
{
// MotionData with HasVelocity flag should surface via CurrentVelocity.
const uint Style = 0x003Du;
const uint Motion = 0x0003u;
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0) —
// without it, initialize_state's SetDefaultState fails, state.Style
// stays 0, and GetObjectSequence's entry guard rejects every
// dispatch. Route the default straight at Motion (the cycle this
// test cares about).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000003u;
const uint AnimId = 0x03000120u;
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
@ -1054,6 +1153,7 @@ public sealed class AnimationSequencerTests
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
var md = new MotionData
{
@ -1077,8 +1177,12 @@ public sealed class AnimationSequencerTests
[Fact]
public void CurrentVelocity_ScaledBySpeedMod()
{
const uint Style = 0x003Du;
const uint Motion = 0x0003u;
// R2-Q4: retail-mandatory StyleDefaults, and Motion needs its
// 0x40000000 cycle-class bit — the same-motion re-speed fast path
// (Branch 2, target==substate) still requires the class-bit gate to
// be reached in the first place.
const uint Style = 0x8000003Du;
const uint Motion = 0x40000003u;
const uint AnimId = 0x03000121u;
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
@ -1086,6 +1190,7 @@ public sealed class AnimationSequencerTests
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
var md = new MotionData
{
@ -1112,27 +1217,37 @@ public sealed class AnimationSequencerTests
// When a non-cyclic link node exhausts and we advance_to_next_animation,
// an AnimationDoneHook should be queued so consumers can react (e.g. UI
// wake-on-idle-complete).
const uint Style = 0x003Du;
const uint IdleMotion = 0x0003u;
const uint WalkMotion = 0x0005u;
// R2-Q4: class-bit-tagged ids (see SetCycle_WithTransitionLink_PrependLinkFrames).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x40000003u;
const uint WalkMotion = 0x40000005u;
const uint IdleAnim = 0x03000132u;
const uint CycleAnim = 0x03000130u;
const uint LinkAnim = 0x03000131u;
var linkAnim = Fixtures.MakeAnim(2, 1, Vector3.Zero, Quaternion.Identity);
var cycleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = Fixtures.MakeMtable(
style: Style, motion: WalkMotion, cycleAnimId: CycleAnim,
fromMotion: IdleMotion, toMotion: WalkMotion, linkAnimId: LinkAnim,
framerate: 10f);
// R2-Q4: retail-mandatory StyleDefaults — route it at IdleMotion with
// its own cycle so the priming SetCycle call below actually dispatches.
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 10f);
var loader = new FakeLoader();
loader.Register(IdleAnim, idleAnim);
loader.Register(CycleAnim, cycleAnim);
loader.Register(LinkAnim, linkAnim);
var seq = new AnimationSequencer(setup, mt, loader);
SetCurrentMotion(seq, Style, IdleMotion);
// Prime as if already playing IdleMotion — real SetCycle call.
seq.SetCycle(Style, IdleMotion);
seq.SetCycle(Style, WalkMotion);
seq.ConsumePendingHooks();
@ -1150,8 +1265,11 @@ public sealed class AnimationSequencerTests
{
// A 10-frame cycle at 10 fps = 1.0s per loop. If we halve the playback
// rate (factor 0.5), advancing 1.0s should produce half a loop (5 frames).
const uint Style = 0x003Du;
const uint Motion = 0x0007u; // RunForward
// R2-Q4: Motion needs the 0x40000000 cycle-class bit — GetObjectSequence
// Branch 2 (and its same-motion fast re-speed path) never triggers on
// a bare low-word id (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000007u; // RunForward
const uint AnimId = 0x03000401u;
// Unique per-frame Z so we can tell where the cursor lands.
@ -1177,6 +1295,11 @@ public sealed class AnimationSequencerTests
Framerate = 10f,
});
mt.Cycles[cycleKey] = md;
// R2-Q4: the dispatch stack needs the retail-mandatory StyleDefaults
// entry (SetDefaultState 0x005230a0 requires StyleDefaults[DefaultStyle];
// GetObjectSequence refuses a zero style/substate). Real dat tables
// always carry it.
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
var loader = new FakeLoader();
loader.Register(AnimId, anim);
@ -1184,8 +1307,11 @@ public sealed class AnimationSequencerTests
var seq = new AnimationSequencer(setup, mt, loader);
seq.SetCycle(Style, Motion, speedMod: 1f);
// Halve the playback rate.
seq.MultiplyCyclicFramerate(0.5f);
// R2-Q4: halve the playback rate via the retail same-motion re-speed
// (GetObjectSequence Branch-2 fast path: change_cycle_speed +
// subtract_motion(old) + combine_motion(new), decomp §5) — the old
// MultiplyCyclicFramerate adapter composite is deleted.
seq.SetCycle(Style, Motion, speedMod: 0.5f);
// 10 frames at 5 fps = 2.0s per loop. Advance 1.0s → cursor ~= frame 5.
seq.Advance(1.0f);
@ -1193,7 +1319,8 @@ public sealed class AnimationSequencerTests
Assert.Single(frames);
Assert.InRange(frames[0].Origin.Z, 4f, 6f);
// Velocity also scales: originally (0,4,0), now (0,2,0).
// Velocity also scales: originally (0,4,0), now (0,2,0)
// (subtract_motion(1.0) + combine_motion(0.5) = ×0.5 net).
Assert.Equal(2f, seq.CurrentVelocity.Y, 1);
}
@ -1202,8 +1329,11 @@ public sealed class AnimationSequencerTests
{
// Changing speed mid-cycle must NOT reset the frame cursor — the
// animation keeps playing from where it was, just faster/slower.
const uint Style = 0x003Du;
const uint Motion = 0x0007u;
// R2-Q4: class-bit-tagged ids — the bare ids made this test pass
// VACUOUSLY (dispatch silently failed; "cursor unchanged" held
// because nothing moved at all).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000007u;
const uint AnimId = 0x03000402u;
var anim = new Animation();
@ -1219,6 +1349,9 @@ public sealed class AnimationSequencerTests
mt.DefaultStyle = (DRWMotionCommand)Style;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(AnimId, framerate: 10f);
// R2-Q4: retail-mandatory StyleDefaults entry (see
// MultiplyCyclicFramerate_HalvesPlaybackRate).
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
var loader = new FakeLoader();
loader.Register(AnimId, anim);
@ -1228,7 +1361,9 @@ public sealed class AnimationSequencerTests
seq.Advance(0.3f); // cursor ~ frame 3
double before = GetFramePosition(seq);
seq.MultiplyCyclicFramerate(2.0f);
// R2-Q4: mid-cycle re-speed via the retail Branch-2 fast path — must
// not touch the cursor (change_cycle_speed scales framerates only).
seq.SetCycle(Style, Motion, speedMod: 2.0f);
double after = GetFramePosition(seq);
Assert.Equal(before, after, 5);
@ -1241,8 +1376,10 @@ public sealed class AnimationSequencerTests
// NOT reset the cursor — it should call MultiplyCyclicFramerate to
// keep the run loop smooth (retail behavior for a mid-run RunRate
// broadcast). Mirror of ACE MotionTable.cs:132-139 fast-path.
const uint Style = 0x003Du;
const uint Motion = 0x0007u;
// R2-Q4: Motion needs the 0x40000000 cycle-class bit — see
// CurrentVelocity_ExposedFromMotionData_WhenHasVelocity.
const uint Style = 0x8000003Du;
const uint Motion = 0x40000007u;
const uint AnimId = 0x03000403u;
var anim = Fixtures.MakeAnim(10, 1, Vector3.Zero, Quaternion.Identity);
@ -1273,14 +1410,17 @@ public sealed class AnimationSequencerTests
// surface as (0,4,0) at speedMod=1.0, (0,6,0) at 1.5×, (0,2,0) at
// 0.5×. The dead-reckoning integrator in TickAnimations reads
// CurrentVelocity each tick, so this has to be accurate.
const uint Style = 0x003Du;
const uint Motion = 0x0007u;
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
const uint Style = 0x8000003Du;
const uint Motion = 0x40000007u;
const uint AnimId = 0x03000405u;
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
var md = new MotionData { Flags = MotionDataFlags.HasVelocity, Velocity = new Vector3(0, 4, 0) };
@ -1316,8 +1456,10 @@ public sealed class AnimationSequencerTests
{
// Guard: the new speed-path must not break the classic
// "identical call = no state change" behavior.
const uint Style = 0x003Du;
const uint Motion = 0x0007u;
// R2-Q4: Motion needs the 0x40000000 cycle-class bit — see
// CurrentVelocity_ExposedFromMotionData_WhenHasVelocity.
const uint Style = 0x8000003Du;
const uint Motion = 0x40000007u;
const uint AnimId = 0x03000404u;
var anim = Fixtures.MakeAnim(10, 1, Vector3.Zero, Quaternion.Identity);
@ -1344,14 +1486,17 @@ public sealed class AnimationSequencerTests
// A turn cycle with MotionData.Omega = (0, 0, 1) rad/sec (yaw)
// should surface as CurrentOmega = (0, 0, 1) after SetCycle.
// Scales with speedMod exactly like Velocity.
const uint Style = 0x003Du;
const uint Motion = 0x000Du; // TurnRight
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
const uint Style = 0x8000003Du;
const uint Motion = 0x4000000Du; // TurnRight
const uint AnimId = 0x03000701u;
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
var md = new MotionData { Flags = MotionDataFlags.HasOmega, Omega = new Vector3(0, 0, 1.0f) };
@ -1380,19 +1525,27 @@ public sealed class AnimationSequencerTests
// reads the cycle's run-speed and moves the entity smoothly.
// Crucial: otherwise remote entities would stutter at every stance
// transition while the link plays.
const uint Style = 0x003Du;
const uint IdleMotion = 0x0003u;
const uint WalkMotion = 0x0005u;
// R2-Q4: class-bit-tagged ids (see SetCycle_WithTransitionLink_PrependLinkFrames).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x40000003u;
const uint WalkMotion = 0x40000005u;
const uint IdleAnim = 0x03000603u;
const uint CycleAnim = 0x03000601u;
const uint LinkAnim = 0x03000602u;
var cycleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
var linkAnim = Fixtures.MakeAnim(2, 1, Vector3.Zero, Quaternion.Identity);
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)WalkMotion;
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
// — route it at IdleMotion (the state we prime through below).
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 10f);
int cycleKey = (int)((Style << 16) | (WalkMotion & 0xFFFFFFu));
var cycleMd = new MotionData { Flags = MotionDataFlags.HasVelocity, Velocity = new Vector3(0, 3.12f, 0) };
@ -1410,11 +1563,13 @@ public sealed class AnimationSequencerTests
mt.Links[linkOuter] = linkCmdData;
var loader = new FakeLoader();
loader.Register(IdleAnim, idleAnim);
loader.Register(CycleAnim, cycleAnim);
loader.Register(LinkAnim, linkAnim);
var seq = new AnimationSequencer(setup, mt, loader);
SetCurrentMotion(seq, Style, IdleMotion);
// Prime as if already playing IdleMotion — real SetCycle call.
seq.SetCycle(Style, IdleMotion);
seq.SetCycle(Style, WalkMotion);
// We just enqueued [link(0)][cycle(3.12 forward)]. Current node is
@ -1438,7 +1593,11 @@ public sealed class AnimationSequencerTests
// An Action-class command (mask 0x10) resolves via the Links dict
// keyed by (style, currentSubstate) → motion. Example: a ThrustMed
// attack while in SwordCombat stance.
const uint Style = 0x003Eu; // SwordCombat
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
// (SetDefaultState 0x005230a0 is retail-mandatory — GetObjectSequence
// refuses style==0/substate==0, see
// CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
const uint Style = 0x8000003Eu; // SwordCombat
const uint IdleMotion = 0x41000003u; // Ready
const uint ActionMotion = 0x10000058u; // ThrustMed (Action class)
const uint IdleAnimId = 0x03000501u;
@ -1451,6 +1610,7 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
@ -1484,7 +1644,9 @@ public sealed class AnimationSequencerTests
// values followed by Ready. Retail keeps currState.Substate at Ready
// while the action link drains, so the Ready echo must not abort the
// in-flight swing.
const uint Style = 0x003Du;
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
// (see PlayAction_Action_ResolvesFromLinksDict).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x41000003u;
const uint AttackMotion = 0x10000052u;
const uint IdleAnimId = 0x03000503u;
@ -1492,6 +1654,7 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable { DefaultStyle = (DRWMotionCommand)Style };
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
@ -1519,11 +1682,19 @@ public sealed class AnimationSequencerTests
[Fact]
public void PlayAction_Modifier_ResolvesFromModifiersDict()
{
// A Modifier-class command (mask 0x20) — like Jump (0x2500003B) —
// resolves from the Modifiers dict, first with style-specific key
// then with unstyled fallback. Empirically: the modifier's anim
// plays on top of the current cycle.
const uint Style = 0x003Du;
// R2-Q4 EXPECTED-DIFF (mirrors AnimationSequencerCutoverTraceTests.
// S9_TurnModifier): a Modifier-class command (mask 0x20) — like Jump
// (0x2500003B) or a turn-while-moving overlay — resolves from the
// Modifiers dict, first with style-specific key then with unstyled
// fallback (CMotionTable.GetObjectSequence Branch 4). Pre-cutover the
// adapter INSERTED the modifier's anim before the cyclic tail — an
// acdream invention. Retail Branch 4 is PHYSICS-ONLY combine_motion:
// the base cycle's anim list is untouched (no new nodes), and the
// modifier contributes velocity/omega on top of the cycle's own,
// tracked on the MotionState modifier stack (AP-73 mechanism).
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
// (see PlayAction_Action_ResolvesFromLinksDict).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x41000003u;
const uint JumpMotion = 0x2500003Bu; // Modifier class
const uint IdleAnimId = 0x03000510u;
@ -1535,12 +1706,18 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
// Modifier: (Style, Jump)
// Modifier: (Style, Jump) — carries an omega contribution (a jump
// kick's angular nudge) rather than an anim payload, since Branch 4
// never touches the anim list.
int modKey = (int)((Style << 16) | (JumpMotion & 0xFFFFFFu));
mt.Modifiers[modKey] = Fixtures.MakeMotionData(JumpAnimId, framerate: 10f);
var jumpMd = Fixtures.MakeMotionData(JumpAnimId, framerate: 10f);
jumpMd.Flags = MotionDataFlags.HasOmega;
jumpMd.Omega = new Vector3(0f, 0f, 2.5f);
mt.Modifiers[modKey] = jumpMd;
var loader = new FakeLoader();
loader.Register(IdleAnimId, idleAnim);
@ -1548,12 +1725,15 @@ public sealed class AnimationSequencerTests
var seq = new AnimationSequencer(setup, mt, loader);
seq.SetCycle(Style, IdleMotion);
int queueBefore = seq.QueueCount;
seq.PlayAction(JumpMotion);
var fr = seq.Advance(0.01f);
Assert.Single(fr);
Assert.Equal(77f, fr[0].Origin.Z, 1);
// No anim nodes inserted — the queue is unchanged from before the
// modifier fired.
Assert.Equal(queueBefore, seq.QueueCount);
// The modifier's omega is combined onto the sequence's physics.
Assert.Equal(2.5f, seq.CurrentOmega.Z, 3);
}
[Fact]
@ -1563,7 +1743,9 @@ public sealed class AnimationSequencerTests
// Action(0x10) | ChatEmote(0x02) | Mappable(0x01). Because the
// Action bit is set, they route through the Links-dict lookup just
// like attacks. Verifies the class-bit math.
const uint Style = 0x003Du;
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
// (see PlayAction_Action_ResolvesFromLinksDict).
const uint Style = 0x8000003Du;
const uint IdleMotion = 0x41000003u;
const uint WaveMotion = 0x13000087u;
const uint IdleAnimId = 0x03000520u;
@ -1575,6 +1757,7 @@ public sealed class AnimationSequencerTests
var setup = Fixtures.MakeSetup(1);
var mt = new MotionTable();
mt.DefaultStyle = (DRWMotionCommand)Style;
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);