acdream/src/AcDream.Core/Physics/Motion/AnimSequenceNode.cs
Erik 1371c2a14c feat(R1-P0/P1): CSequence research base + verbatim AnimSequenceNode
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>
2026-07-02 19:45:56 +02:00

159 lines
5.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 &lt; 0</c> test.</item>
/// <item><see cref="SetAnimationId"/> clamps in retail's exact order
/// (0x00525d60): high&lt;0 → num1; low≥num → num1; high≥num → num1;
/// low&gt;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];
}
}