using System; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.Core.Physics.Motion; /// /// R1-P1 — verbatim port of retail's AnimSequenceNode (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 CSequence's animation list: a resolved dat /// plus a playable frame window /// (..) and a signed /// whose SIGN is retail's playback-direction flag. /// /// Retail semantics preserved exactly (gap-map items G1/G2/G16/G18): /// /// The boundary pair ( / /// ) is direction-aware and returns BARE /// integers — retail has NO epsilon here (0x00525c80/0x00525cb0). ACE's /// PhysicsGlobals.EPSILON subtraction compensates for ACE's own /// float FrameNumber and must not be copied (P0-pins.md). /// SWAPS and /// when the factor is negative (0x00525be0) — /// coupled with the boundary pair's framerate < 0 test. /// clamps in retail's exact order /// (0x00525d60): high<0 → num−1; low≥num → num−1; high≥num → num−1; /// low>high → high=low. /// returns null out of range /// (retail; ACE's identity-frame return is an ACE-ism). /// /// /// Unlike the legacy AnimationSequencer.AnimNode, retail nodes carry /// NO per-node IsLooping/Velocity/Omega — loop membership is list structure /// (first_cyclic) and physics accumulators live on the sequence /// (G16). /// public sealed class AnimSequenceNode { /// Resolved dat animation, or null (id 0 / missing). public Animation? Anim { get; private set; } /// /// Frames per second; NEGATIVE means reverse playback (retail's /// direction flag). Default 30f (0x00525d30). /// public float Framerate = 30f; /// Inclusive window low bound. Default −1 (0x00525d30). public int LowFrame = -1; /// Inclusive window high bound; −1 = "to the end" sentinel /// resolved by . Default −1. public int HighFrame = -1; public bool HasAnim => Anim is not null; /// Default ctor — retail defaults (0x00525d30). public AnimSequenceNode() { } /// /// Ctor from a MotionData entry (0x00525f90): /// copy framerate/low/high, then resolve + clamp via /// . /// public AnimSequenceNode(AnimData animData, IAnimationLoader loader) { Framerate = animData.Framerate; LowFrame = animData.LowFrame; HighFrame = animData.HighFrame; SetAnimationId((uint)animData.AnimId, loader); } /// /// Resolve the dat animation and clamp the frame window /// (0x00525d60). The clamp block runs only when an animation resolved; /// order is retail-exact. /// 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; } /// /// Direction-aware starting boundary (0x00525c80): reverse playback /// starts at high_frame + 1, forward at low_frame. /// BARE int — no epsilon (G1). /// public int GetStartingFrame() => Framerate < 0f ? HighFrame + 1 : LowFrame; /// /// Direction-aware ending boundary (0x00525cb0): reverse ends at /// low_frame, forward at high_frame + 1. BARE int. /// public int GetEndingFrame() => Framerate < 0f ? LowFrame : HighFrame + 1; /// /// 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. /// public void MultiplyFramerate(float factor) { if (factor < 0f) { (LowFrame, HighFrame) = (HighFrame, LowFrame); } Framerate *= factor; } /// /// Root-motion frame at a double position (0x005247b0): floor then the /// int overload. /// public Frame? GetPosFrame(double frameNumber) => GetPosFrame((int)Math.Floor(frameNumber)); /// /// Root-motion frame by index (0x00525c10): null when no animation, /// index out of range, or the animation carries no PosFrames. /// 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]; } /// /// Skeletal part frame by index — same bounds discipline as /// . /// public AnimationFrame? GetPartFrame(int index) { if (Anim is null || index < 0 || index >= Anim.PartFrames.Count) return null; return Anim.PartFrames[index]; } }