using System;
using System.Collections.Generic;
using System.Numerics;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics.Motion;
///
/// R1-P4 host seam standing in for retail's CPhysicsObj.anim_hooks
/// SmartArray + the global AnimDoneHook singleton
/// (add_anim_hook 0x00514c20; process_hooks 0x00511550 drains
/// once per physics tick — the drain point stays with the host until R6
/// places it per retail's UpdateObjectInternal order).
///
public interface IAnimHookQueue
{
/// Queue a matched AnimFrame hook (already direction-filtered
/// by execute_hooks).
void AddAnimHook(DatReaderWriter.Types.AnimationHook hook);
/// Queue the global animation-done hook — retail's
/// AnimDoneHook::Execute → CPhysicsObj::Hook_AnimDone →
/// CPartArray::AnimationDone(1) chain (R2 consumes it as
/// MotionDone).
void AddAnimDoneHook();
}
///
/// R1-P2 — verbatim port of retail's CSequence container + list
/// surgery (Phase R plan stage R1; oracle
/// `docs/research/2026-07-02-r1-csequence/r1-csequence-decomp.md` §1-§17,
/// §20, §24). The per-tick advance (`update`/`update_internal`/
/// `apply_physics`/hook dispatch) lands in R1-P3/P4.
///
/// Structure: a doubly-linked animation-node list with two cursors —
/// (the node currently playing) and
/// (where the looping tail begins; everything
/// before it is one-shot "link" animation). Retail invariant (G10):
/// append_animation slides first_cyclic to the JUST-APPENDED
/// node on EVERY call — the cyclic tail is always exactly the last node
/// appended so far.
///
/// Physics accumulators (/) live on
/// the SEQUENCE, not per node (G16); retail replaces them via
/// set_velocity/set_omega and algebraically blends via
/// combine_physics/subtract_physics (the R2 fast path's mechanism).
///
/// Divergence register (rows added with this commit):
///
/// - frame_number is x87 long double in retail
/// (acclient.h:30747); C# double is the closest available (G15).
/// - The intrusive DLList is a managed ;
/// node identity semantics preserved via
/// references.
///
///
public sealed class CSequence
{
private readonly LinkedList _animList = new(); // anim_list (DLList)
private LinkedListNode? _firstCyclic; // first_cyclic
private LinkedListNode? _currAnim; // curr_anim
private readonly IAnimationLoader _loader;
/// Fractional frame position within .
/// Retail x87 long double → double (register row, G15).
public double FrameNumber;
/// Sequence root-motion velocity accumulator (body-local).
public Vector3 Velocity;
/// Sequence angular-velocity accumulator.
public Vector3 Omega;
/// Static pose used when no animation node is active
/// (placement_frame, §16).
public AnimationFrame? PlacementFrame { get; private set; }
public uint PlacementFrameId { get; private set; }
public CSequence(IAnimationLoader loader) => _loader = loader;
// ── inspection surface (adapter + tests) ────────────────────────────
public AnimSequenceNode? CurrAnim => _currAnim?.Value;
public AnimSequenceNode? FirstCyclic => _firstCyclic?.Value;
public int Count => _animList.Count;
/// has_anims (0x00524bd0).
public bool HasAnims() => _animList.Count > 0;
/// TEST SEAM: reposition curr_anim by list index (retail state
/// reached via update_internal, which lands in P4).
public void SetCurrAnimForTest(int index)
{
var n = _animList.First;
for (int i = 0; i < index && n != null; i++) n = n.Next;
_currAnim = n;
}
// ── append_animation (0x00525510, §24) ──────────────────────────────
///
/// Append a MotionData anim entry. A node whose dat animation fails to
/// resolve is discarded. first_cyclic slides to the appended
/// node on EVERY call; curr_anim seeds to the head (with
/// frame_number = get_starting_frame()) only when it was null.
///
public void AppendAnimation(AnimData animData)
{
var node = new AnimSequenceNode(animData, _loader);
if (!node.HasAnim)
return; // retail deletes the node — discard
_animList.AddLast(node);
_firstCyclic = _animList.Last;
if (_currAnim is null)
{
_currAnim = _animList.First;
FrameNumber = _currAnim!.Value.GetStartingFrame();
}
}
// ── clear family (§3-§5) ────────────────────────────────────────────
/// clear (0x005255b0): full wipe INCLUDING the placement
/// fields — the raw body resets them (the "2-instruction clear" note in
/// the gap map was wrong; raw decomp is authority).
public void Clear()
{
ClearAnimations();
ClearPhysics();
PlacementFrame = null;
PlacementFrameId = 0;
}
/// clear_animations (0x00524dc0): delete every node,
/// null both cursors, zero frame_number.
public void ClearAnimations()
{
_animList.Clear();
_firstCyclic = null;
_currAnim = null;
FrameNumber = 0.0;
}
/// clear_physics (0x00524d50).
public void ClearPhysics()
{
Velocity = Vector3.Zero;
Omega = Vector3.Zero;
}
// ── remove family (§6-§8) ───────────────────────────────────────────
///
/// remove_cyclic_anims (0x00524e40): delete first_cyclic
/// → tail. A removed curr_anim snaps BACK to the previous node
/// with frame_number = prev.get_ending_frame() (or 0.0 when the
/// list emptied). Afterwards first_cyclic = new tail (or null).
///
public void RemoveCyclicAnims()
{
var node = _firstCyclic;
while (node is not null)
{
var next = node.Next;
if (ReferenceEquals(_currAnim, node))
{
var prev = node.Previous;
_currAnim = prev;
FrameNumber = prev is null ? 0.0 : prev.Value.GetEndingFrame();
}
_animList.Remove(node);
node = next;
}
_firstCyclic = _animList.Last;
}
///
/// remove_link_animations(count) (0x00524be0): delete up to
/// predecessors of first_cyclic. A
/// removed curr_anim snaps FORWARD to first_cyclic with
/// frame_number = get_starting_frame().
///
public void RemoveLinkAnimations(int count)
{
for (int i = 0; i < count; i++)
{
var prev = _firstCyclic?.Previous;
if (prev is null)
break;
if (ReferenceEquals(_currAnim, prev))
{
_currAnim = _firstCyclic;
if (_firstCyclic is not null)
FrameNumber = _firstCyclic.Value.GetStartingFrame();
}
_animList.Remove(prev);
}
}
/// remove_all_link_animations (0x00524ca0): loop until
/// first_cyclic has no predecessor.
public void RemoveAllLinkAnimations()
{
while (_firstCyclic?.Previous is not null)
RemoveLinkAnimations(1);
}
///
/// apricot (0x00524b40; the PDB-verified retail name): trim
/// consumed nodes from the head, bounded by BOTH curr_anim
/// (stop — still live) and first_cyclic (defensive bound —
/// never delete into the cyclic tail). Called after every update (§22).
///
public void Apricot()
{
var head = _animList.First;
if (head is null || ReferenceEquals(head, _currAnim))
return;
while (!ReferenceEquals(head, _firstCyclic))
{
_animList.Remove(head!);
head = _animList.First;
if (head is null || ReferenceEquals(head, _currAnim))
break;
}
}
// ── physics accumulators (§10-§13) ──────────────────────────────────
public void SetVelocity(Vector3 v) => Velocity = v; // 0x00524880
public void SetOmega(Vector3 w) => Omega = w; // 0x005248a0
public void CombinePhysics(Vector3 v, Vector3 w) { Velocity += v; Omega += w; } // 0x005248c0
public void SubtractPhysics(Vector3 v, Vector3 w) { Velocity -= v; Omega -= w; } // 0x00524900
// ── multiply_cyclic_animation_fr (0x00524940, §14) ──────────────────
///
/// Scale the framerate of every node from first_cyclic to the
/// tail. Framerates ONLY (G13) — retail rescales the sequence
/// velocity/omega separately via change_cycle_speed's
/// subtract_motion/combine_motion composite (R2).
///
public void MultiplyCyclicAnimationFramerate(float factor)
{
for (var n = _firstCyclic; n is not null; n = n.Next)
n.Value.MultiplyFramerate(factor);
}
// ── placement + accessors (§15-§17) ─────────────────────────────────
/// set_placement_frame (0x005249b0).
public void SetPlacementFrame(AnimationFrame? frame, uint id)
{
PlacementFrame = frame;
PlacementFrameId = id;
}
/// get_curr_animframe (0x00524970): the floored current
/// part frame, or the placement frame when no node is active.
public AnimationFrame? GetCurrAnimframe()
{
if (_currAnim is null)
return PlacementFrame;
return _currAnim.Value.GetPartFrame((int)Math.Floor(FrameNumber));
}
/// get_curr_frame_number (0x005249d0).
public int GetCurrFrameNumber() => (int)Math.Floor(FrameNumber);
// ── apply_physics (0x00524ab0, §19) ─────────────────────────────────
///
/// Accumulated-physics root motion: advance by
/// / over a signed quantum —
/// MAGNITUDE from , SIGN from
/// (retail copysign semantics; call sites
/// pass 1/framerate as magnitude and the signed elapsed time as sign).
///
public void ApplyPhysics(Frame frame, double quantum, double signSource)
{
double signed = Math.Abs(quantum);
if (signSource < 0.0)
signed = -signed;
float sq = (float)signed;
frame.Origin += Velocity * sq;
FrameOps.Rotate(frame, Omega * sq);
}
// ── R1-P4: the frame-advance core ───────────────────────────────────
/// Host hook queue (hook_obj); null = hooks dropped
/// (objects without a physics host).
public IAnimHookQueue? HookObj;
///
/// CSequence::update (0x00525b80, §22): non-empty list →
/// then ; empty list
/// with a Frame → accumulated-physics free motion.
///
public void Update(double timeElapsed, Frame? frame)
{
if (_animList.Count > 0 && _currAnim is not null)
{
UpdateInternal(timeElapsed, frame);
Apricot();
}
else if (frame is not null)
{
ApplyPhysics(frame, timeElapsed, timeElapsed);
}
}
///
/// CSequence::update_internal (0x005255d0, §21 — ACE-verified
/// skeleton, P0-pins.md): advance by
/// framerate·dt; on overshoot clamp to the RAW high/low frame, compute
/// the leftover time, and mark animDone; fire the pose/physics/hook
/// triple for EVERY crossed integer frame (ascending forward,
/// descending reverse); queue the global AnimDoneHook when the list
/// HEAD is no longer the cyclic tail (G5's structural gate); advance to
/// the next node and LOOP with the carried leftover (P0 pin).
/// Iterative (retail while-true), NO safety cap (G4), NO boundary
/// epsilon (G1/G3).
///
public void UpdateInternal(double timeElapsed, Frame? frame)
{
while (true)
{
if (_currAnim is null)
return;
var currAnim = _currAnim.Value;
double framerate = currAnim.Framerate;
double frametime = framerate * timeElapsed;
int lastFrame = (int)Math.Floor(FrameNumber);
FrameNumber += frametime;
double frameTimeElapsed = 0.0;
bool animDone = false;
if (frametime > 0.0)
{
if (currAnim.HighFrame < Math.Floor(FrameNumber))
{
double frameOffset = FrameNumber - currAnim.HighFrame - 1.0;
if (frameOffset < 0.0)
frameOffset = 0.0;
if (Math.Abs(framerate) > FrameOps.FEpsilon)
frameTimeElapsed = frameOffset / framerate;
FrameNumber = currAnim.HighFrame;
animDone = true;
}
while (Math.Floor(FrameNumber) > lastFrame)
{
if (frame is not null)
{
var pos = currAnim.GetPosFrame(lastFrame);
if (pos is not null)
FrameOps.Combine(frame, pos);
if (Math.Abs(framerate) > FrameOps.FEpsilon)
ApplyPhysics(frame, 1.0 / framerate, timeElapsed);
}
ExecuteHooks(currAnim.GetPartFrame(lastFrame), +1);
lastFrame++;
}
}
else if (frametime < 0.0)
{
if (currAnim.LowFrame > Math.Floor(FrameNumber))
{
double frameOffset = FrameNumber - currAnim.LowFrame;
if (frameOffset > 0.0)
frameOffset = 0.0;
if (Math.Abs(framerate) > FrameOps.FEpsilon)
frameTimeElapsed = frameOffset / framerate;
FrameNumber = currAnim.LowFrame;
animDone = true;
}
while (Math.Floor(FrameNumber) < lastFrame)
{
if (frame is not null)
{
var pos = currAnim.GetPosFrame(lastFrame);
if (pos is not null)
FrameOps.Subtract1(frame, pos);
if (Math.Abs(framerate) > FrameOps.FEpsilon)
ApplyPhysics(frame, 1.0 / framerate, timeElapsed);
}
ExecuteHooks(currAnim.GetPartFrame(lastFrame), -1);
lastFrame--;
}
}
else
{
if (frame is not null && Math.Abs(timeElapsed) > FrameOps.FEpsilon)
ApplyPhysics(frame, timeElapsed, timeElapsed);
return;
}
if (!animDone)
return;
// AnimDone gate: a LIST-STRUCTURE test, not a node flag (G5) —
// fire only when the consumed head is not already the cyclic
// tail (retail 0x00525943-0x00525968).
if (HookObj is not null
&& _animList.First is not null
&& !ReferenceEquals(_animList.First, _firstCyclic))
{
HookObj.AddAnimDoneHook();
}
AdvanceToNextAnimation(timeElapsed, frame);
timeElapsed = frameTimeElapsed; // the P0-pinned leftover carry
}
}
///
/// CSequence::advance_to_next_animation (0x005252b0, §23): four
/// pose operations per transition — un-apply the outgoing node's pose
/// (subtract1 + residual physics), step (forward wraps to
/// first_cyclic; REVERSE wraps to the LIST TAIL — asymmetric by
/// design), reseed from the incoming node's
/// direction-aware boundary, apply the incoming pose (combine +
/// physics). Pose ops run in BOTH directions (ACE's framerate-sign
/// gates are ACE-isms; the decomp is unconditional apart from the
/// degenerate-framerate guard).
///
public void AdvanceToNextAnimation(double timeElapsed, Frame? frame)
{
if (_currAnim is null)
return;
var outgoing = _currAnim.Value;
// (a) un-apply the outgoing node's pose at the CURRENT FrameNumber.
if (frame is not null && Math.Abs(outgoing.Framerate) >= FrameOps.FEpsilon)
{
var pos = outgoing.GetPosFrame((int)FrameNumber);
if (pos is not null)
FrameOps.Subtract1(frame, pos);
ApplyPhysics(frame, 1.0 / outgoing.Framerate, timeElapsed);
}
// (b) step; (c) reseed FrameNumber from the incoming boundary.
if (timeElapsed >= 0.0)
{
_currAnim = _currAnim.Next ?? _firstCyclic;
if (_currAnim is null)
return;
FrameNumber = _currAnim.Value.GetStartingFrame();
}
else
{
_currAnim = _currAnim.Previous ?? _animList.Last;
if (_currAnim is null)
return;
FrameNumber = _currAnim.Value.GetEndingFrame();
}
// (d) apply the incoming node's pose at the new FrameNumber.
var incoming = _currAnim.Value;
if (frame is not null && Math.Abs(incoming.Framerate) >= FrameOps.FEpsilon)
{
var pos = incoming.GetPosFrame((int)FrameNumber);
if (pos is not null)
FrameOps.Combine(frame, pos);
ApplyPhysics(frame, 1.0 / incoming.Framerate, timeElapsed);
}
}
///
/// CSequence::execute_hooks (0x00524830, §18): QUEUE the part
/// frame's hooks whose direction is Both (0) or matches the playback
/// direction (+1 forward / −1 backward) into the host. Null part frame
/// guarded (retail has a latent null-deref here — documented safe
/// divergence, G18).
///
private void ExecuteHooks(AnimationFrame? partFrame, int direction)
{
if (partFrame is null || HookObj is null)
return;
foreach (var hook in partFrame.Hooks)
{
if (hook is null)
continue;
int dir = (int)hook.Direction;
if (dir == 0 || dir == direction)
HookObj.AddAnimHook(hook);
}
}
// ── internal cursors for P4 (update_internal operates on nodes) ─────
internal LinkedListNode? CurrAnimNode
{
get => _currAnim;
set => _currAnim = value;
}
internal LinkedListNode? FirstCyclicNode => _firstCyclic;
internal LinkedList AnimList => _animList;
}