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; }