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:
parent
9147344a6f
commit
a987cad182
3 changed files with 46 additions and 67 deletions
|
|
@ -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)
|
## 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
|
omega accumulators (set/combine/subtract), update/update_internal root
|
||||||
motion, apply_physics, placement frames, hook dispatch. Goldens: dat
|
motion, apply_physics, placement frames, hook dispatch. Goldens: dat
|
||||||
MotionData fixtures + a cdb trace of append_animation/remove_cyclic_anims
|
MotionData fixtures + a cdb trace of append_animation/remove_cyclic_anims
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,8 @@ public readonly struct PartTransform
|
||||||
/// var seq = new AnimationSequencer(setup, motionTable, dats);
|
/// var seq = new AnimationSequencer(setup, motionTable, dats);
|
||||||
/// seq.SetCycle(style, motion, speedMod);
|
/// seq.SetCycle(style, motion, speedMod);
|
||||||
/// // each frame:
|
/// // 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 hooks = seq.ConsumePendingHooks(); // fire audio / VFX / damage
|
||||||
/// var root = seq.ConsumeRootMotionDelta(); // add to AFrame if desired
|
|
||||||
/// </code>
|
/// </code>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -223,16 +222,9 @@ public sealed class AnimationSequencer
|
||||||
// AdapterHookQueue seam below); drained via ConsumePendingHooks.
|
// AdapterHookQueue seam below); drained via ConsumePendingHooks.
|
||||||
private readonly List<AnimationHook> _pendingHooks = new();
|
private readonly List<AnimationHook> _pendingHooks = new();
|
||||||
|
|
||||||
// Root motion (PosFrames) delta accumulated during Advance. Drained via
|
// R1-P6: root motion flows through the Advance(dt, Frame) overload
|
||||||
// ConsumeRootMotionDelta. R1-P6 NOTE (gap map G7): the accumulation
|
// (retail CSequence::update(Frame*), 0x00525b80) — the old adapter
|
||||||
// sites that used to write these fields are gone with the deleted
|
// accumulator fields are gone with ConsumeRootMotionDelta.
|
||||||
// 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;
|
|
||||||
|
|
||||||
// ── Diagnostics (Commit A 2026-05-03) ───────────────────────────────────
|
// ── Diagnostics (Commit A 2026-05-03) ───────────────────────────────────
|
||||||
// Throttle clock for the [SCFAST] / [SCFULL] / [SCNULLFALLBACK] log lines
|
// 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.
|
/// If no animation is loaded, all parts get identity transforms.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public IReadOnlyList<PartTransform> Advance(float dt)
|
public IReadOnlyList<PartTransform> Advance(float dt)
|
||||||
|
=> Advance(dt, rootMotionFrame: null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R1-P6 (gap map G7): the root-motion overload. When
|
||||||
|
/// <paramref name="rootMotionFrame"/> is supplied, the core's
|
||||||
|
/// <c>update</c>/<c>update_internal</c> apply BOTH root-motion sources
|
||||||
|
/// into it exactly as retail's <c>CPartArray::Update</c> path does —
|
||||||
|
/// the per-crossed-frame <c>PosFrames</c> combine/subtract AND the
|
||||||
|
/// sequence velocity/omega via <c>apply_physics</c> (0x00524ab0). This
|
||||||
|
/// is the seam R6's retail per-tick order consumes
|
||||||
|
/// (<c>UpdatePositionInternal → CPartArray.Update → adjust_offset →
|
||||||
|
/// Frame.combine</c>).
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PartTransform> Advance(float dt, Frame? rootMotionFrame)
|
||||||
{
|
{
|
||||||
int partCount = _setup.Parts.Count;
|
int partCount = _setup.Parts.Count;
|
||||||
|
|
||||||
if (_core.CurrAnim == null || dt <= 0f)
|
if (_core.CurrAnim == null && rootMotionFrame is null)
|
||||||
return BuildIdentityFrame(partCount);
|
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
|
_core.Update(dt, rootMotionFrame);
|
||||||
// (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);
|
|
||||||
|
|
||||||
return BuildBlendedFrame();
|
return _core.CurrAnim == null
|
||||||
|
? BuildIdentityFrame(partCount)
|
||||||
|
: BuildBlendedFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -734,24 +742,10 @@ public sealed class AnimationSequencer
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// R1-P6: ConsumeRootMotionDelta DELETED (zero external callers; gap map
|
||||||
/// Retrieve and clear the root-motion displacement accumulated from
|
// API-migration table). Root motion flows through the
|
||||||
/// <see cref="Animation.PosFrames"/> during the last <see cref="Advance"/>
|
// Advance(dt, Frame) overload — retail's CSequence::update(Frame*)
|
||||||
/// calls. Returns (Zero, Identity) — see the R1-P5 note on
|
// contract — for R6's per-tick wiring.
|
||||||
/// <see cref="_rootMotionPos"/>: this accumulator has zero live writers
|
|
||||||
/// after the cutover (root motion now flows only through
|
|
||||||
/// <see cref="CSequence.Update"/>'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).
|
|
||||||
/// </summary>
|
|
||||||
public (Vector3 Position, Quaternion Rotation) ConsumeRootMotionDelta()
|
|
||||||
{
|
|
||||||
var result = (_rootMotionPos, _rootMotionRot);
|
|
||||||
_rootMotionPos = Vector3.Zero;
|
|
||||||
_rootMotionRot = Quaternion.Identity;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Play a one-shot action/modifier motion (Jump, emote, attack, etc.)
|
/// Play a one-shot action/modifier motion (Jump, emote, attack, etc.)
|
||||||
|
|
@ -895,8 +889,6 @@ public sealed class AnimationSequencer
|
||||||
{
|
{
|
||||||
_core.Clear();
|
_core.Clear();
|
||||||
_pendingHooks.Clear();
|
_pendingHooks.Clear();
|
||||||
_rootMotionPos = Vector3.Zero;
|
|
||||||
_rootMotionRot = Quaternion.Identity;
|
|
||||||
CurrentStyle = 0;
|
CurrentStyle = 0;
|
||||||
CurrentMotion = 0;
|
CurrentMotion = 0;
|
||||||
CurrentSpeedMod = 1f;
|
CurrentSpeedMod = 1f;
|
||||||
|
|
|
||||||
|
|
@ -997,29 +997,17 @@ 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; retired R1-P5) ─────────────────────
|
// ── PosFrames root motion (R1-P6: the wired Frame path, gap map G7) ───────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ConsumeRootMotionDelta_AlwaysZero_AccumulatorUnwiredPendingP6()
|
public void Advance_WithRootMotionFrame_AccumulatesPosFrameDeltas()
|
||||||
{
|
{
|
||||||
// R1-P5 (2026-07-02): pre-cutover, Advance's own hand-rolled
|
// R1-P6 (2026-07-02): root motion flows through retail's actual
|
||||||
// per-frame-crossing loop wrote _rootMotionPos/_rootMotionRot
|
// contract — CSequence::update(quantum, Frame*) (0x00525b80):
|
||||||
// directly from Animation.PosFrames (AFrame.Combine/Subtract). That
|
// every crossed integer frame combines the node's pos_frame into
|
||||||
// loop is DELETED — frame advance now runs entirely inside
|
// the caller-supplied Frame (update_internal 0x005255d0). The old
|
||||||
// CSequence.Update, and root motion in retail flows through
|
// adapter-side accumulator (ConsumeRootMotionDelta) is DELETED —
|
||||||
// CSequence.apply_physics(Frame*, ...) (0x00524ab0), which needs an
|
// this is the seam R6's per-tick order consumes.
|
||||||
// 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;
|
||||||
|
|
@ -1043,15 +1031,14 @@ public sealed class AnimationSequencerTests
|
||||||
|
|
||||||
var seq = new AnimationSequencer(setup, mt, loader);
|
var seq = new AnimationSequencer(setup, mt, loader);
|
||||||
seq.SetCycle(Style, Motion);
|
seq.SetCycle(Style, Motion);
|
||||||
seq.ConsumeRootMotionDelta(); // clear
|
|
||||||
|
|
||||||
// Advance 0.25s → 2.5 frames → 2 crossings (0→1, 1→2). Pre-cutover
|
// Advance 0.25s @10fps → 2.5 frames → 2 crossings (0→1, 1→2), each
|
||||||
// this would have accumulated ~2.0 on X; post-cutover it's inert.
|
// combining +1 X of pos_frame origin into the supplied Frame.
|
||||||
seq.Advance(0.25f);
|
var rootFrame = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
|
||||||
var (pos, rot) = seq.ConsumeRootMotionDelta();
|
seq.Advance(0.25f, rootFrame);
|
||||||
|
|
||||||
Assert.Equal(Vector3.Zero, pos);
|
Assert.True(rootFrame.Origin.X >= 1.8f && rootFrame.Origin.X <= 2.2f,
|
||||||
Assert.Equal(Quaternion.Identity, rot);
|
$"Expected ~2.0 root motion X after 2 crossings via the wired Frame, got {rootFrame.Origin.X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue