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
209
tests/AcDream.Core.Tests/Physics/Motion/AnimSequenceNodeTests.cs
Normal file
209
tests/AcDream.Core.Tests/Physics/Motion/AnimSequenceNodeTests.cs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R1-P1 — verbatim <c>AnimSequenceNode</c> (Phase R plan, gap-map items
|
||||
/// G1/G2/G16/G18). Oracle: r1-csequence-decomp.md §25-28 (ctors 0x00525d30 /
|
||||
/// 0x00525f90, set_animation_id 0x00525d60, get_starting_frame 0x00525c80,
|
||||
/// get_ending_frame 0x00525cb0, multiply_framerate 0x00525be0, get_pos_frame
|
||||
/// 0x005247b0 / 0x00525c10).
|
||||
///
|
||||
/// KEY RETAIL SEMANTICS UNDER TEST:
|
||||
/// - boundary pair is DIRECTION-AWARE and returns BARE INTS with NO epsilon
|
||||
/// (ACE's epsilon subtraction is an ACE fabrication — P0-pins.md);
|
||||
/// - multiply_framerate SWAPS low/high on a negative factor;
|
||||
/// - set_animation_id clamps in a fixed order (high<0 → num−1;
|
||||
/// low≥num → num−1; high≥num → num−1; low>high → high=low).
|
||||
/// </summary>
|
||||
public class AnimSequenceNodeTests
|
||||
{
|
||||
private sealed class FakeLoader : IAnimationLoader
|
||||
{
|
||||
private readonly Animation? _anim;
|
||||
public FakeLoader(Animation? anim) => _anim = anim;
|
||||
public Animation? LoadAnimation(uint id) => _anim;
|
||||
}
|
||||
|
||||
private static Animation MakeAnim(int numFrames, bool posFrames = false)
|
||||
{
|
||||
var anim = new Animation();
|
||||
for (int f = 0; f < numFrames; f++)
|
||||
{
|
||||
var pf = new AnimationFrame(1u);
|
||||
pf.Frames.Add(new Frame { Origin = new Vector3(f, 0, 0), Orientation = Quaternion.Identity });
|
||||
anim.PartFrames.Add(pf);
|
||||
if (posFrames)
|
||||
anim.PosFrames.Add(new Frame { Origin = new Vector3(0, f, 0), Orientation = Quaternion.Identity });
|
||||
}
|
||||
return anim;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultCtor_RetailDefaults()
|
||||
{
|
||||
// 0x00525d30: framerate=30f, low=-1, high=-1, anim=null.
|
||||
var n = new AnimSequenceNode();
|
||||
Assert.Equal(30f, n.Framerate);
|
||||
Assert.Equal(-1, n.LowFrame);
|
||||
Assert.Equal(-1, n.HighFrame);
|
||||
Assert.False(n.HasAnim);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAnimationId_Zero_LeavesAnimNullAndFramesUnclamped()
|
||||
{
|
||||
// 0x00525d60: arg2==0 → anim=null; the clamp block is gated on
|
||||
// anim != null, so low/high stay at their raw values.
|
||||
var n = new AnimSequenceNode();
|
||||
n.SetAnimationId(0, new FakeLoader(MakeAnim(10)));
|
||||
Assert.False(n.HasAnim);
|
||||
Assert.Equal(-1, n.LowFrame);
|
||||
Assert.Equal(-1, n.HighFrame);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// low, high (pre-clamp), numFrames, expectedLow, expectedHigh
|
||||
[InlineData(0, -1, 10, 0, 9)] // high<0 → num-1 ("play to end")
|
||||
[InlineData(12, -1, 10, 9, 9)] // high<0 first, then low>=num → num-1
|
||||
[InlineData(3, 15, 10, 3, 9)] // high>=num → num-1
|
||||
[InlineData(15, 15, 10, 9, 9)] // both clamp to num-1
|
||||
[InlineData(5, 2, 10, 5, 5)] // low>high after clamps → high=low
|
||||
[InlineData(2, 7, 10, 2, 7)] // in-range untouched
|
||||
public void SetAnimationId_ClampOrder(int low, int high, int numFrames, int expLow, int expHigh)
|
||||
{
|
||||
var n = new AnimSequenceNode { LowFrame = low, HighFrame = high };
|
||||
n.SetAnimationId(0x0300ABCDu, new FakeLoader(MakeAnim(numFrames)));
|
||||
Assert.True(n.HasAnim);
|
||||
Assert.Equal(expLow, n.LowFrame);
|
||||
Assert.Equal(expHigh, n.HighFrame);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// framerate, low, high, expectedStart, expectedEnd — BARE INTS, no epsilon
|
||||
[InlineData(30f, 2, 7, 2, 8)] // forward: start=low, end=high+1
|
||||
[InlineData(-30f, 2, 7, 8, 2)] // reverse: start=high+1, end=low
|
||||
[InlineData(0f, 2, 7, 2, 8)] // zero framerate is NOT < 0 → forward
|
||||
[InlineData(30f, 0, 0, 0, 1)] // single-frame forward
|
||||
[InlineData(-30f, 0, 0, 1, 0)] // single-frame reverse
|
||||
public void BoundaryPair_DirectionAware_BareInts(
|
||||
float framerate, int low, int high, int expStart, int expEnd)
|
||||
{
|
||||
var n = new AnimSequenceNode { Framerate = framerate, LowFrame = low, HighFrame = high };
|
||||
Assert.Equal(expStart, n.GetStartingFrame());
|
||||
Assert.Equal(expEnd, n.GetEndingFrame());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiplyFramerate_Positive_NoSwap()
|
||||
{
|
||||
var n = new AnimSequenceNode { Framerate = 30f, LowFrame = 2, HighFrame = 7 };
|
||||
n.MultiplyFramerate(2f);
|
||||
Assert.Equal(60f, n.Framerate);
|
||||
Assert.Equal(2, n.LowFrame);
|
||||
Assert.Equal(7, n.HighFrame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiplyFramerate_Negative_SwapsLowHigh()
|
||||
{
|
||||
// 0x00525be0: factor < 0 → swap(low_frame, high_frame), then
|
||||
// framerate *= factor. Coupled with the direction-aware boundary
|
||||
// pair (framerate now negative reads the swapped fields).
|
||||
var n = new AnimSequenceNode { Framerate = 30f, LowFrame = 2, HighFrame = 7 };
|
||||
n.MultiplyFramerate(-1f);
|
||||
Assert.Equal(-30f, n.Framerate);
|
||||
Assert.Equal(7, n.LowFrame);
|
||||
Assert.Equal(2, n.HighFrame);
|
||||
// Reverse playback boundaries against the SWAPPED fields:
|
||||
// start = high_frame(2)+1 = 3?? — no: framerate<0 → start = high+1
|
||||
// where high_frame is now 2 → 3; end = low_frame = 7.
|
||||
Assert.Equal(3, n.GetStartingFrame());
|
||||
Assert.Equal(7, n.GetEndingFrame());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiplyFramerate_DoubleNegative_RoundTrips()
|
||||
{
|
||||
var n = new AnimSequenceNode { Framerate = 30f, LowFrame = 2, HighFrame = 7 };
|
||||
n.MultiplyFramerate(-1f);
|
||||
n.MultiplyFramerate(-1f);
|
||||
Assert.Equal(30f, n.Framerate);
|
||||
Assert.Equal(2, n.LowFrame);
|
||||
Assert.Equal(7, n.HighFrame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPosFrame_NullAnim_ReturnsNull()
|
||||
{
|
||||
var n = new AnimSequenceNode();
|
||||
Assert.Null(n.GetPosFrame(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPosFrame_OutOfRange_ReturnsNull()
|
||||
{
|
||||
// 0x00525c10: retail returns NULL out of range (ACE returns identity
|
||||
// — an ACE-ism, gap G18: port the null).
|
||||
var n = new AnimSequenceNode();
|
||||
n.SetAnimationId(1, new FakeLoader(MakeAnim(3, posFrames: true)));
|
||||
Assert.Null(n.GetPosFrame(-1));
|
||||
Assert.Null(n.GetPosFrame(3));
|
||||
Assert.NotNull(n.GetPosFrame(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPosFrame_DoubleOverload_Floors()
|
||||
{
|
||||
// 0x005247b0: floor(double) → int overload.
|
||||
var n = new AnimSequenceNode();
|
||||
n.SetAnimationId(1, new FakeLoader(MakeAnim(3, posFrames: true)));
|
||||
var f = n.GetPosFrame(1.99);
|
||||
Assert.NotNull(f);
|
||||
Assert.Equal(1f, f!.Origin.Y); // frame index 1, not 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPosFrame_AnimWithoutPosFrames_ReturnsNull()
|
||||
{
|
||||
var n = new AnimSequenceNode();
|
||||
n.SetAnimationId(1, new FakeLoader(MakeAnim(3, posFrames: false)));
|
||||
Assert.Null(n.GetPosFrame(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPartFrame_BoundsAndValue()
|
||||
{
|
||||
var n = new AnimSequenceNode();
|
||||
n.SetAnimationId(1, new FakeLoader(MakeAnim(3)));
|
||||
Assert.Null(n.GetPartFrame(-1));
|
||||
Assert.Null(n.GetPartFrame(3));
|
||||
var pf = n.GetPartFrame(2);
|
||||
Assert.NotNull(pf);
|
||||
Assert.Equal(2f, pf!.Frames[0].Origin.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CtorFromAnimData_CopiesThenClamps()
|
||||
{
|
||||
// 0x00525f90: copies framerate/low/high from AnimData then runs
|
||||
// set_animation_id (which clamps against the resolved anim).
|
||||
QualifiedDataId<Animation> qid = 0x03001234u;
|
||||
var ad = new AnimData
|
||||
{
|
||||
AnimId = qid,
|
||||
LowFrame = 0,
|
||||
HighFrame = -1,
|
||||
Framerate = 15f,
|
||||
};
|
||||
var n = new AnimSequenceNode(ad, new FakeLoader(MakeAnim(5)));
|
||||
Assert.Equal(15f, n.Framerate);
|
||||
Assert.Equal(0, n.LowFrame);
|
||||
Assert.Equal(4, n.HighFrame); // -1 sentinel clamped to num-1
|
||||
Assert.True(n.HasAnim);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue