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>
This commit is contained in:
parent
cae56afc82
commit
1371c2a14c
7 changed files with 3363 additions and 0 deletions
159
src/AcDream.Core/Physics/Motion/AnimSequenceNode.cs
Normal file
159
src/AcDream.Core/Physics/Motion/AnimSequenceNode.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
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];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue