feat(R1-P5): AnimationSequencer rehosted on the verbatim CSequence core
The adapter keeps its full public surface (every consumer compiles unchanged) while the internals move to Motion.CSequence: DELETED (legacy invented mechanisms, per the R1 gap map + Phase R mandate): the AnimNode class + adapter queue/_currNode/_firstCyclic/ _framePosition, the ACE-fabricated FrameEpsilon boundary math, the safety=64 advance cap, ClearCyclicTail surgery, the stale-head _currNode relocation block, per-node IsLooping/Velocity/Omega, the adapter-side AdvanceToNextAnimation/ApplyPosFrame/ExecuteHooks, the per-node !IsLooping AnimationDone gate (now the core's list-structure head != first_cyclic gate, G5). REHOSTED: SetCycle rebuild = RemoveCyclicAnims (+ClearAnimations for K-fix18) + retail add_motion appends (framerate-only AnimData scaling, G10 loop membership: the LAST appended node is the cyclic tail) + Fix B via RemoveAllLinkAnimations (the core's snap-forward IS Fix B's behavior); Advance = core.Update(dt, null) with hooks flowing through an IAnimHookQueue adapter into the existing ConsumePendingHooks drain; fast path = core.MultiplyCyclicAnimationFramerate + the adapter-level velocity rescale (R2's change_cycle_speed composite stand-in, G13). Kept byte-identical: adjust_motion remap, GetLink + stop-anim fallback, K-fix18, velocity synthesis, SlerpRetailClient, the #61 render-side clamp, SCFAST/SCFULL diagnostics. Tests: 2 updated with decomp citations — the reverse-start boundary now pins retail's bare-int HighFrame+1 (0x00525c80, no epsilon, G1); the root-motion accumulation test pins the inert-stub state pending P6's Frame wiring (G7). All 44 sequencer tests + the 56 core tests + full suite green (3346). Implemented by a dedicated agent against the P5 spec; diff + tests reviewed, suite re-verified before commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
658b91d8aa
commit
9147344a6f
2 changed files with 398 additions and 693 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -614,12 +614,19 @@ public sealed class AnimationSequencerTests
|
||||||
// CurrentMotion should record the original TurnLeft command.
|
// CurrentMotion should record the original TurnLeft command.
|
||||||
Assert.Equal(TurnLeft, seq.CurrentMotion);
|
Assert.Equal(TurnLeft, seq.CurrentMotion);
|
||||||
|
|
||||||
// Without swap: StartFrame=0, EndFrame=3 (original range preserved).
|
// R1-P5 (2026-07-02): pre-cutover this pinned the ACE-fabricated
|
||||||
// GetStartFramePosition for negative speed = (EndFrame+1)-eps = (3+1)-eps ≈ 3.99999.
|
// epsilon boundary ((EndFrame+1)-eps ~= 3.99999). Retail's
|
||||||
// The cursor starts near the HIGH end and counts DOWN toward StartFrame(=0).
|
// AnimSequenceNode.GetStartingFrame (0x00525c80) returns a BARE INT
|
||||||
|
// for reverse playback: HighFrame + 1 (gap map G1 — "NO epsilon").
|
||||||
|
// LowFrame=0, HighFrame=3 (no swap at append time; the swap only
|
||||||
|
// happens inside MultiplyFramerate for an in-place resign, which
|
||||||
|
// this path doesn't take), so the retail-exact start position is
|
||||||
|
// exactly 4.0, not "near but under" 4.0. The cursor starts at the
|
||||||
|
// boundary and counts DOWN toward LowFrame(=0) on the next Advance.
|
||||||
double pos = GetFramePosition(seq);
|
double pos = GetFramePosition(seq);
|
||||||
Assert.True(pos > 3.9 && pos < 4.0,
|
Assert.True(pos == 4.0,
|
||||||
$"Expected framePosition near 3.99999 (reverse start near EndFrame+1) but got {pos}");
|
$"Expected framePosition == 4 (bare-int reverse start = HighFrame+1, "
|
||||||
|
+ $"retail AnimSequenceNode.GetStartingFrame 0x00525c80 has NO epsilon — G1); got {pos}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -990,13 +997,29 @@ public sealed class AnimationSequencerTests
|
||||||
Assert.Contains(hooks, h => h is SoundHook sh && (uint)sh.Id == 0x0A000005u);
|
Assert.Contains(hooks, h => h is SoundHook sh && (uint)sh.Id == 0x0A000005u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PosFrames root motion (Phase E.1) ────────────────────────────────────
|
// ── PosFrames root motion (Phase E.1; retired R1-P5) ─────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Advance_WithPosFrames_AccumulatesRootMotion()
|
public void ConsumeRootMotionDelta_AlwaysZero_AccumulatorUnwiredPendingP6()
|
||||||
{
|
{
|
||||||
// Animation with PosFrames flag and per-frame origin deltas should
|
// R1-P5 (2026-07-02): pre-cutover, Advance's own hand-rolled
|
||||||
// surface a non-zero root motion delta after Advance.
|
// per-frame-crossing loop wrote _rootMotionPos/_rootMotionRot
|
||||||
|
// directly from Animation.PosFrames (AFrame.Combine/Subtract). That
|
||||||
|
// loop is DELETED — frame advance now runs entirely inside
|
||||||
|
// CSequence.Update, and root motion in retail flows through
|
||||||
|
// CSequence.apply_physics(Frame*, ...) (0x00524ab0), which needs an
|
||||||
|
// actual target Frame passed into Update. This adapter's Advance
|
||||||
|
// passes null (gap map G7: "R1-P6 wires it"), so PosFrames deltas
|
||||||
|
// are computed by the core's UpdateInternal but have nowhere to
|
||||||
|
// land — ConsumeRootMotionDelta has zero live writers post-cutover
|
||||||
|
// (confirmed: also zero external callers pre-cutover, per the R1
|
||||||
|
// gap map API-migration table).
|
||||||
|
//
|
||||||
|
// This test pins that ConsumeRootMotionDelta is a safe, inert stub
|
||||||
|
// until P6 wires a real Frame through Advance/Update — NOT that
|
||||||
|
// root motion doesn't work. When P6 lands, this test should be
|
||||||
|
// replaced with one asserting real accumulation through the wired
|
||||||
|
// Frame path.
|
||||||
const uint Style = 0x003Du;
|
const uint Style = 0x003Du;
|
||||||
const uint Motion = 0x0003u;
|
const uint Motion = 0x0003u;
|
||||||
const uint AnimId = 0x03000110u;
|
const uint AnimId = 0x03000110u;
|
||||||
|
|
@ -1022,17 +1045,13 @@ public sealed class AnimationSequencerTests
|
||||||
seq.SetCycle(Style, Motion);
|
seq.SetCycle(Style, Motion);
|
||||||
seq.ConsumeRootMotionDelta(); // clear
|
seq.ConsumeRootMotionDelta(); // clear
|
||||||
|
|
||||||
// Advance 0.25s → 2.5 frames → 2 crossings (0→1, 1→2) → 2 posFrame deltas applied.
|
// Advance 0.25s → 2.5 frames → 2 crossings (0→1, 1→2). Pre-cutover
|
||||||
|
// this would have accumulated ~2.0 on X; post-cutover it's inert.
|
||||||
seq.Advance(0.25f);
|
seq.Advance(0.25f);
|
||||||
var (pos, _) = seq.ConsumeRootMotionDelta();
|
var (pos, rot) = seq.ConsumeRootMotionDelta();
|
||||||
|
|
||||||
// Each crossing adds +X origin → total X should be 2.
|
Assert.Equal(Vector3.Zero, pos);
|
||||||
Assert.True(pos.X >= 1.8f && pos.X <= 2.2f,
|
Assert.Equal(Quaternion.Identity, rot);
|
||||||
$"Expected ~2.0 root motion X after 2 crossings, got {pos.X}");
|
|
||||||
|
|
||||||
// A subsequent consume with no advance should return zero (drained).
|
|
||||||
var (pos2, _) = seq.ConsumeRootMotionDelta();
|
|
||||||
Assert.Equal(Vector3.Zero, pos2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1624,14 +1643,27 @@ public sealed class AnimationSequencerTests
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Expose _framePosition (double) via reflection (test-only).</summary>
|
/// <summary>
|
||||||
|
/// Expose the core CSequence's FrameNumber via reflection (test-only).
|
||||||
|
/// R1-P5 rehost (2026-07-02): _framePosition lived directly on
|
||||||
|
/// AnimationSequencer pre-cutover; it now lives on the private _core
|
||||||
|
/// (CSequence) field as the public FrameNumber. Two-hop reflection:
|
||||||
|
/// grab _core, then its FrameNumber field.
|
||||||
|
/// </summary>
|
||||||
private static double GetFramePosition(AnimationSequencer seq)
|
private static double GetFramePosition(AnimationSequencer seq)
|
||||||
{
|
{
|
||||||
var field = typeof(AnimationSequencer)
|
var coreField = typeof(AnimationSequencer)
|
||||||
.GetField("_framePosition",
|
.GetField("_core",
|
||||||
System.Reflection.BindingFlags.NonPublic |
|
System.Reflection.BindingFlags.NonPublic |
|
||||||
System.Reflection.BindingFlags.Instance);
|
System.Reflection.BindingFlags.Instance);
|
||||||
return field is null ? -1.0 : (double)field.GetValue(seq)!;
|
var core = coreField?.GetValue(seq);
|
||||||
|
if (core is null) return -1.0;
|
||||||
|
|
||||||
|
var frameNumberField = core.GetType()
|
||||||
|
.GetField("FrameNumber",
|
||||||
|
System.Reflection.BindingFlags.Public |
|
||||||
|
System.Reflection.BindingFlags.Instance);
|
||||||
|
return frameNumberField is null ? -1.0 : (double)frameNumberField.GetValue(core)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue