feat(R1-P6): root-motion Frame seam + dead-API removal — R1 COMPLETE

- Advance(dt, Frame? rootMotionFrame) overload: retail's actual root-
  motion contract (CSequence::update(quantum, Frame*) 0x00525b80) —
  every crossed frame's pos_frame combines into the caller's Frame plus
  the sequence velocity/omega via apply_physics. This is the seam R6's
  retail per-tick order (CPartArray.Update -> adjust_offset ->
  Frame.combine) consumes.
- DELETED: ConsumeRootMotionDelta + the dead adapter accumulator fields
  (zero external callers; gap-map API-migration table).
- Root-motion test now asserts REAL accumulation through the wired
  Frame path (replaces the P5 inert-stub pin).
- Phase R plan: R1 stage marked SHIPPED with its commit trail.

Full suite green (3346). R1 done: P0 research/pins -> P1 node -> P2
container -> P3 physics -> P4 advance core -> P5 adapter cutover ->
P6 wiring. Next: R2 (GetObjectSequence + MotionTableManager; extraction
workflow already running).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 20:24:01 +02:00
parent 9147344a6f
commit a987cad182
3 changed files with 46 additions and 67 deletions

View file

@ -997,29 +997,17 @@ public sealed class AnimationSequencerTests
Assert.Contains(hooks, h => h is SoundHook sh && (uint)sh.Id == 0x0A000005u);
}
// ── PosFrames root motion (Phase E.1; retired R1-P5) ─────────────────────
// ── PosFrames root motion (R1-P6: the wired Frame path, gap map G7) ───────
[Fact]
public void ConsumeRootMotionDelta_AlwaysZero_AccumulatorUnwiredPendingP6()
public void Advance_WithRootMotionFrame_AccumulatesPosFrameDeltas()
{
// R1-P5 (2026-07-02): pre-cutover, Advance's own hand-rolled
// 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.
// R1-P6 (2026-07-02): root motion flows through retail's actual
// contract — CSequence::update(quantum, Frame*) (0x00525b80):
// every crossed integer frame combines the node's pos_frame into
// the caller-supplied Frame (update_internal 0x005255d0). The old
// adapter-side accumulator (ConsumeRootMotionDelta) is DELETED —
// this is the seam R6's per-tick order consumes.
const uint Style = 0x003Du;
const uint Motion = 0x0003u;
const uint AnimId = 0x03000110u;
@ -1043,15 +1031,14 @@ public sealed class AnimationSequencerTests
var seq = new AnimationSequencer(setup, mt, loader);
seq.SetCycle(Style, Motion);
seq.ConsumeRootMotionDelta(); // clear
// 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);
var (pos, rot) = seq.ConsumeRootMotionDelta();
// Advance 0.25s @10fps → 2.5 frames → 2 crossings (0→1, 1→2), each
// combining +1 X of pos_frame origin into the supplied Frame.
var rootFrame = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
seq.Advance(0.25f, rootFrame);
Assert.Equal(Vector3.Zero, pos);
Assert.Equal(Quaternion.Identity, rot);
Assert.True(rootFrame.Origin.X >= 1.8f && rootFrame.Origin.X <= 2.2f,
$"Expected ~2.0 root motion X after 2 crossings via the wired Frame, got {rootFrame.Origin.X}");
}
[Fact]