From a987cad1828f0f4b5ff4e6cb52160e22bdc11398 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 2 Jul 2026 20:24:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(R1-P6):=20root-motion=20Frame=20seam=20+?= =?UTF-8?q?=20dead-API=20removal=20=E2=80=94=20R1=20COMPLETE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ...6-07-02-retail-motion-animation-rewrite.md | 2 +- .../Physics/AnimationSequencer.cs | 70 ++++++++----------- .../Physics/AnimationSequencerTests.cs | 41 ++++------- 3 files changed, 46 insertions(+), 67 deletions(-) diff --git a/docs/plans/2026-07-02-retail-motion-animation-rewrite.md b/docs/plans/2026-07-02-retail-motion-animation-rewrite.md index 1a1d3efe..a32beffa 100644 --- a/docs/plans/2026-07-02-retail-motion-animation-rewrite.md +++ b/docs/plans/2026-07-02-retail-motion-animation-rewrite.md @@ -73,7 +73,7 @@ the 300 ms stop-detection window, NPC UP hard-snaps. ## Stage plan (each: pseudocode → harness → port → cutover → DELETE legacy → register sweep) -- **R1 — CSequence verbatim.** Node list, framerate/rate math, velocity+ +- **R1 — CSequence verbatim. SHIPPED 2026-07-02** (commits 1371c2a1 P0/P1, 778744bf P2, 5138b8fb P3, 658b91d8 P4, 9147344a P5, +P6): the verbatim core (AnimSequenceNode/CSequence/FrameOps, 56 conformance tests) + the AnimationSequencer adapter rehost deleting the legacy epsilon/stale-head/safety-cap/per-node-flag mechanisms; root motion flows through Advance(dt, Frame) = retail update(Frame*). Registers AD-33/AD-34. Node list, framerate/rate math, velocity+ omega accumulators (set/combine/subtract), update/update_internal root motion, apply_physics, placement frames, hook dispatch. Goldens: dat MotionData fixtures + a cdb trace of append_animation/remove_cyclic_anims diff --git a/src/AcDream.Core/Physics/AnimationSequencer.cs b/src/AcDream.Core/Physics/AnimationSequencer.cs index 417eb539..b830c049 100644 --- a/src/AcDream.Core/Physics/AnimationSequencer.cs +++ b/src/AcDream.Core/Physics/AnimationSequencer.cs @@ -110,9 +110,8 @@ public readonly struct PartTransform /// var seq = new AnimationSequencer(setup, motionTable, dats); /// seq.SetCycle(style, motion, speedMod); /// // each frame: -/// var transforms = seq.Advance(dt); +/// var transforms = seq.Advance(dt, rootMotionFrame); // root motion lands in the Frame /// var hooks = seq.ConsumePendingHooks(); // fire audio / VFX / damage -/// var root = seq.ConsumeRootMotionDelta(); // add to AFrame if desired /// /// /// @@ -223,16 +222,9 @@ public sealed class AnimationSequencer // AdapterHookQueue seam below); drained via ConsumePendingHooks. private readonly List _pendingHooks = new(); - // Root motion (PosFrames) delta accumulated during Advance. Drained via - // ConsumeRootMotionDelta. R1-P6 NOTE (gap map G7): the accumulation - // sites that used to write these fields are gone with the deleted - // legacy advance loop — root motion now flows ONLY through the core's - // CSequence.Update(frame) path, which R1-P6 wires to an actual Frame. - // ConsumeRootMotionDelta has zero callers today (confirmed at cutover) - // so it's kept as a no-op-returning stub for API compatibility until - // P6 retires or rewires it. - private Vector3 _rootMotionPos; - private Quaternion _rootMotionRot = Quaternion.Identity; + // R1-P6: root motion flows through the Advance(dt, Frame) overload + // (retail CSequence::update(Frame*), 0x00525b80) — the old adapter + // accumulator fields are gone with ConsumeRootMotionDelta. // ── Diagnostics (Commit A 2026-05-03) ─────────────────────────────────── // Throttle clock for the [SCFAST] / [SCFULL] / [SCNULLFALLBACK] log lines @@ -704,19 +696,35 @@ public sealed class AnimationSequencer /// If no animation is loaded, all parts get identity transforms. /// public IReadOnlyList Advance(float dt) + => Advance(dt, rootMotionFrame: null); + + /// + /// R1-P6 (gap map G7): the root-motion overload. When + /// is supplied, the core's + /// update/update_internal apply BOTH root-motion sources + /// into it exactly as retail's CPartArray::Update path does — + /// the per-crossed-frame PosFrames combine/subtract AND the + /// sequence velocity/omega via apply_physics (0x00524ab0). This + /// is the seam R6's retail per-tick order consumes + /// (UpdatePositionInternal → CPartArray.Update → adjust_offset → + /// Frame.combine). + /// + public IReadOnlyList Advance(float dt, Frame? rootMotionFrame) { int partCount = _setup.Parts.Count; - if (_core.CurrAnim == null || dt <= 0f) + if (_core.CurrAnim == null && rootMotionFrame is null) return BuildIdentityFrame(partCount); + if (dt <= 0f) + return _core.CurrAnim == null + ? BuildIdentityFrame(partCount) + : BuildBlendedFrame(); - // R1-P6 wires a real Frame* through here for root-motion output - // (gap map G7). Until then, pass null — the core still advances - // frame_number, fires hooks, and steps nodes; it just skips the - // apply_physics/pos-frame combine calls that need a target Frame. - _core.Update(dt, null); + _core.Update(dt, rootMotionFrame); - return BuildBlendedFrame(); + return _core.CurrAnim == null + ? BuildIdentityFrame(partCount) + : BuildBlendedFrame(); } /// @@ -734,24 +742,10 @@ public sealed class AnimationSequencer return result; } - /// - /// Retrieve and clear the root-motion displacement accumulated from - /// during the last - /// calls. Returns (Zero, Identity) — see the R1-P5 note on - /// : this accumulator has zero live writers - /// after the cutover (root motion now flows only through - /// 's Frame* argument, unwired until P6; - /// gap map G7). Kept for API compatibility; P6 either wires a real - /// accumulation path or deletes this method (it already has zero - /// external callers). - /// - public (Vector3 Position, Quaternion Rotation) ConsumeRootMotionDelta() - { - var result = (_rootMotionPos, _rootMotionRot); - _rootMotionPos = Vector3.Zero; - _rootMotionRot = Quaternion.Identity; - return result; - } + // R1-P6: ConsumeRootMotionDelta DELETED (zero external callers; gap map + // API-migration table). Root motion flows through the + // Advance(dt, Frame) overload — retail's CSequence::update(Frame*) + // contract — for R6's per-tick wiring. /// /// Play a one-shot action/modifier motion (Jump, emote, attack, etc.) @@ -895,8 +889,6 @@ public sealed class AnimationSequencer { _core.Clear(); _pendingHooks.Clear(); - _rootMotionPos = Vector3.Zero; - _rootMotionRot = Quaternion.Identity; CurrentStyle = 0; CurrentMotion = 0; CurrentSpeedMod = 1f; diff --git a/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs b/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs index 2e29abb9..606bc130 100644 --- a/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs +++ b/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs @@ -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]