P0 — research + pins: full CSequence-family verbatim extraction (1756 lines, per-function raw pseudo-C + cleaned flow, decomp line anchors), ACE cross-reference (9 ranked divergences; headline: retail frame_number is x87 long double — ACE's float is the worst case, our double the best available; ACE's frame-boundary epsilon is an ACE fabrication, NOT retail), current-sequencer map, and the R1 gap map (20 gaps, 13 keeps, P0-P6 port order). Pinned the one decomp ambiguity (leftover-time carry after advance_to_next_animation — ACE reading adopted; cdb confirmation protocol recorded, non-blocking). P1 — AnimSequenceNode verbatim (gap G1/G2/G16/G18): - direction-aware BARE-INT boundary pair (get_starting_frame 0x00525c80 / get_ending_frame 0x00525cb0): reverse starts at high+1, ends at low — NO epsilon; - multiply_framerate (0x00525be0) swaps low/high on negative factor; - set_animation_id (0x00525d60) retail clamp order (high<0 -> num-1; low>=num -> num-1; high>=num -> num-1; low>high -> high=low); - ctors with retail defaults (30f/-1/-1; AnimData copy + clamp); - get_pos_frame null out-of-range (retail; ACE returns identity), floor double overload; get_part_frame same discipline; - NO per-node IsLooping/Velocity/Omega — loop membership is list structure, physics accumulators live on the sequence (G16). 22 conformance tests (clamp table, boundary mirror table, swap round-trip, bounds/floor semantics). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
159 lines
5.8 KiB
C#
159 lines
5.8 KiB
C#
using System;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Types;
|
||
|
||
namespace AcDream.Core.Physics.Motion;
|
||
|
||
/// <summary>
|
||
/// R1-P1 — verbatim port of retail's <c>AnimSequenceNode</c> (Phase R plan
|
||
/// `docs/plans/2026-07-02-retail-motion-animation-rewrite.md`, stage R1;
|
||
/// oracle `docs/research/2026-07-02-r1-csequence/r1-csequence-decomp.md`
|
||
/// §25-28).
|
||
///
|
||
/// One node of a <c>CSequence</c>'s animation list: a resolved dat
|
||
/// <see cref="Animation"/> plus a playable frame window
|
||
/// (<see cref="LowFrame"/>..<see cref="HighFrame"/>) and a signed
|
||
/// <see cref="Framerate"/> whose SIGN is retail's playback-direction flag.
|
||
///
|
||
/// Retail semantics preserved exactly (gap-map items G1/G2/G16/G18):
|
||
/// <list type="bullet">
|
||
/// <item>The boundary pair (<see cref="GetStartingFrame"/> /
|
||
/// <see cref="GetEndingFrame"/>) is direction-aware and returns BARE
|
||
/// integers — retail has NO epsilon here (0x00525c80/0x00525cb0). ACE's
|
||
/// <c>PhysicsGlobals.EPSILON</c> subtraction compensates for ACE's own
|
||
/// <c>float FrameNumber</c> and must not be copied (P0-pins.md).</item>
|
||
/// <item><see cref="MultiplyFramerate"/> SWAPS <see cref="LowFrame"/> and
|
||
/// <see cref="HighFrame"/> when the factor is negative (0x00525be0) —
|
||
/// coupled with the boundary pair's <c>framerate < 0</c> test.</item>
|
||
/// <item><see cref="SetAnimationId"/> clamps in retail's exact order
|
||
/// (0x00525d60): high<0 → num−1; low≥num → num−1; high≥num → num−1;
|
||
/// low>high → high=low.</item>
|
||
/// <item><see cref="GetPosFrame(int)"/> returns null out of range
|
||
/// (retail; ACE's identity-frame return is an ACE-ism).</item>
|
||
/// </list>
|
||
///
|
||
/// Unlike the legacy <c>AnimationSequencer.AnimNode</c>, retail nodes carry
|
||
/// NO per-node IsLooping/Velocity/Omega — loop membership is list structure
|
||
/// (<c>first_cyclic</c>) and physics accumulators live on the sequence
|
||
/// (G16).
|
||
/// </summary>
|
||
public sealed class AnimSequenceNode
|
||
{
|
||
/// <summary>Resolved dat animation, or null (id 0 / missing).</summary>
|
||
public Animation? Anim { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Frames per second; NEGATIVE means reverse playback (retail's
|
||
/// direction flag). Default 30f (0x00525d30).
|
||
/// </summary>
|
||
public float Framerate = 30f;
|
||
|
||
/// <summary>Inclusive window low bound. Default −1 (0x00525d30).</summary>
|
||
public int LowFrame = -1;
|
||
|
||
/// <summary>Inclusive window high bound; −1 = "to the end" sentinel
|
||
/// resolved by <see cref="SetAnimationId"/>. Default −1.</summary>
|
||
public int HighFrame = -1;
|
||
|
||
public bool HasAnim => Anim is not null;
|
||
|
||
/// <summary>Default ctor — retail defaults (0x00525d30).</summary>
|
||
public AnimSequenceNode()
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ctor from a MotionData <see cref="AnimData"/> entry (0x00525f90):
|
||
/// copy framerate/low/high, then resolve + clamp via
|
||
/// <see cref="SetAnimationId"/>.
|
||
/// </summary>
|
||
public AnimSequenceNode(AnimData animData, IAnimationLoader loader)
|
||
{
|
||
Framerate = animData.Framerate;
|
||
LowFrame = animData.LowFrame;
|
||
HighFrame = animData.HighFrame;
|
||
SetAnimationId((uint)animData.AnimId, loader);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resolve the dat animation and clamp the frame window
|
||
/// (0x00525d60). The clamp block runs only when an animation resolved;
|
||
/// order is retail-exact.
|
||
/// </summary>
|
||
public void SetAnimationId(uint animId, IAnimationLoader loader)
|
||
{
|
||
Anim = animId == 0 ? null : loader.LoadAnimation(animId);
|
||
|
||
if (Anim is null)
|
||
return;
|
||
|
||
int numFrames = Anim.PartFrames.Count;
|
||
|
||
if (HighFrame < 0)
|
||
HighFrame = numFrames - 1;
|
||
if (LowFrame >= numFrames)
|
||
LowFrame = numFrames - 1;
|
||
if (HighFrame >= numFrames)
|
||
HighFrame = numFrames - 1;
|
||
if (LowFrame > HighFrame)
|
||
HighFrame = LowFrame;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Direction-aware starting boundary (0x00525c80): reverse playback
|
||
/// starts at <c>high_frame + 1</c>, forward at <c>low_frame</c>.
|
||
/// BARE int — no epsilon (G1).
|
||
/// </summary>
|
||
public int GetStartingFrame() => Framerate < 0f ? HighFrame + 1 : LowFrame;
|
||
|
||
/// <summary>
|
||
/// Direction-aware ending boundary (0x00525cb0): reverse ends at
|
||
/// <c>low_frame</c>, forward at <c>high_frame + 1</c>. BARE int.
|
||
/// </summary>
|
||
public int GetEndingFrame() => Framerate < 0f ? LowFrame : HighFrame + 1;
|
||
|
||
/// <summary>
|
||
/// Scale playback rate (0x00525be0): a NEGATIVE factor swaps
|
||
/// low/high before multiplying — the swapped fields plus the
|
||
/// now-negative framerate are how retail encodes reversed windows.
|
||
/// </summary>
|
||
public void MultiplyFramerate(float factor)
|
||
{
|
||
if (factor < 0f)
|
||
{
|
||
(LowFrame, HighFrame) = (HighFrame, LowFrame);
|
||
}
|
||
Framerate *= factor;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Root-motion frame at a double position (0x005247b0): floor then the
|
||
/// int overload.
|
||
/// </summary>
|
||
public Frame? GetPosFrame(double frameNumber)
|
||
=> GetPosFrame((int)Math.Floor(frameNumber));
|
||
|
||
/// <summary>
|
||
/// Root-motion frame by index (0x00525c10): null when no animation,
|
||
/// index out of range, or the animation carries no PosFrames.
|
||
/// </summary>
|
||
public Frame? GetPosFrame(int index)
|
||
{
|
||
if (Anim is null || index < 0 || index >= Anim.PartFrames.Count)
|
||
return null;
|
||
if (Anim.PosFrames is null || index >= Anim.PosFrames.Count)
|
||
return null;
|
||
return Anim.PosFrames[index];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skeletal part frame by index — same bounds discipline as
|
||
/// <see cref="GetPosFrame(int)"/>.
|
||
/// </summary>
|
||
public AnimationFrame? GetPartFrame(int index)
|
||
{
|
||
if (Anim is null || index < 0 || index >= Anim.PartFrames.Count)
|
||
return null;
|
||
return Anim.PartFrames[index];
|
||
}
|
||
}
|