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:
parent
cd0289bea2
commit
3b9d9bb6be
6 changed files with 562 additions and 646 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue