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

@ -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
/// </code>
/// </para>
/// </summary>
@ -223,16 +222,9 @@ public sealed class AnimationSequencer
// AdapterHookQueue seam below); drained via ConsumePendingHooks.
private readonly List<AnimationHook> _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.
/// </returns>
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;
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();
}
/// <summary>
@ -734,24 +742,10 @@ public sealed class AnimationSequencer
return result;
}
/// <summary>
/// Retrieve and clear the root-motion displacement accumulated from
/// <see cref="Animation.PosFrames"/> during the last <see cref="Advance"/>
/// calls. Returns (Zero, Identity) — see the R1-P5 note on
/// <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;
}
// 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.
/// <summary>
/// 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;