feat(R1-P2): verbatim CSequence container + list surgery

CSequence (Core/Physics/Motion): anim-node list with retail's exact
cursor semantics —
- append_animation (0x00525510): first_cyclic slides to the JUST-
  APPENDED node on EVERY call (the cyclic tail is always the last
  appended node); curr_anim seeds to head + get_starting_frame only
  when null; unresolvable anims discarded (G10);
- remove_cyclic_anims (0x00524e40): removed curr_anim snaps BACK to
  prev at get_ending_frame (or 0.0); first_cyclic = new tail;
- remove_link_animations/remove_all_link_animations (0x00524be0/
  0x00524ca0): removed curr_anim snaps FORWARD to first_cyclic at
  get_starting_frame (G11);
- apricot (0x00524b40, PDB-verified retail name): consumed-head trim
  bounded by curr_anim AND first_cyclic;
- clear (0x005255b0) resets placement fields too — raw body is
  authority over the gap map's G20 note;
- sequence-level velocity/omega with set/combine/subtract (G12);
- multiply_cyclic_animation_fr touches framerates ONLY (G13 —
  velocity rescale belongs to R2's change_cycle_speed composite);
- placement frame family + floored accessors (G14).

Register: AD-33 (double vs x87 long double frame_number, G15),
AD-34 (managed LinkedList vs intrusive DLList).

17 list-surgery state-table tests; 39 total R1 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 19:50:19 +02:00
parent 1371c2a14c
commit 778744bf3e
3 changed files with 592 additions and 1 deletions

View file

@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics.Motion;
/// <summary>
/// R1-P2 — verbatim port of retail's <c>CSequence</c> 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 —
/// <see cref="CurrAnim"/> (the node currently playing) and
/// <see cref="FirstCyclic"/> (where the looping tail begins; everything
/// before it is one-shot "link" animation). Retail invariant (G10):
/// <c>append_animation</c> slides <c>first_cyclic</c> to the JUST-APPENDED
/// node on EVERY call — the cyclic tail is always exactly the last node
/// appended so far.
///
/// Physics accumulators (<see cref="Velocity"/>/<see cref="Omega"/>) live on
/// the SEQUENCE, not per node (G16); retail replaces them via
/// <c>set_velocity/set_omega</c> and algebraically blends via
/// <c>combine_physics/subtract_physics</c> (the R2 fast path's mechanism).
///
/// Divergence register (rows added with this commit):
/// <list type="bullet">
/// <item><c>frame_number</c> is x87 <c>long double</c> in retail
/// (acclient.h:30747); C# <c>double</c> is the closest available (G15).</item>
/// <item>The intrusive DLList is a managed <see cref="LinkedList{T}"/>;
/// node identity semantics preserved via <see cref="LinkedListNode{T}"/>
/// references.</item>
/// </list>
/// </summary>
public sealed class CSequence
{
private readonly LinkedList<AnimSequenceNode> _animList = new(); // anim_list (DLList)
private LinkedListNode<AnimSequenceNode>? _firstCyclic; // first_cyclic
private LinkedListNode<AnimSequenceNode>? _currAnim; // curr_anim
private readonly IAnimationLoader _loader;
/// <summary>Fractional frame position within <see cref="CurrAnim"/>.
/// Retail x87 long double → double (register row, G15).</summary>
public double FrameNumber;
/// <summary>Sequence root-motion velocity accumulator (body-local).</summary>
public Vector3 Velocity;
/// <summary>Sequence angular-velocity accumulator.</summary>
public Vector3 Omega;
/// <summary>Static pose used when no animation node is active
/// (<c>placement_frame</c>, §16).</summary>
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;
/// <summary><c>has_anims</c> (0x00524bd0).</summary>
public bool HasAnims() => _animList.Count > 0;
/// <summary>TEST SEAM: reposition curr_anim by list index (retail state
/// reached via update_internal, which lands in P4).</summary>
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) ──────────────────────────────
/// <summary>
/// Append a MotionData anim entry. A node whose dat animation fails to
/// resolve is discarded. <c>first_cyclic</c> slides to the appended
/// node on EVERY call; <c>curr_anim</c> seeds to the head (with
/// <c>frame_number = get_starting_frame()</c>) only when it was null.
/// </summary>
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) ────────────────────────────────────────────
/// <summary><c>clear</c> (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).</summary>
public void Clear()
{
ClearAnimations();
ClearPhysics();
PlacementFrame = null;
PlacementFrameId = 0;
}
/// <summary><c>clear_animations</c> (0x00524dc0): delete every node,
/// null both cursors, zero <c>frame_number</c>.</summary>
public void ClearAnimations()
{
_animList.Clear();
_firstCyclic = null;
_currAnim = null;
FrameNumber = 0.0;
}
/// <summary><c>clear_physics</c> (0x00524d50).</summary>
public void ClearPhysics()
{
Velocity = Vector3.Zero;
Omega = Vector3.Zero;
}
// ── remove family (§6-§8) ───────────────────────────────────────────
/// <summary>
/// <c>remove_cyclic_anims</c> (0x00524e40): delete <c>first_cyclic</c>
/// → tail. A removed <c>curr_anim</c> snaps BACK to the previous node
/// with <c>frame_number = prev.get_ending_frame()</c> (or 0.0 when the
/// list emptied). Afterwards <c>first_cyclic</c> = new tail (or null).
/// </summary>
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;
}
/// <summary>
/// <c>remove_link_animations(count)</c> (0x00524be0): delete up to
/// <paramref name="count"/> predecessors of <c>first_cyclic</c>. A
/// removed <c>curr_anim</c> snaps FORWARD to <c>first_cyclic</c> with
/// <c>frame_number = get_starting_frame()</c>.
/// </summary>
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);
}
}
/// <summary><c>remove_all_link_animations</c> (0x00524ca0): loop until
/// <c>first_cyclic</c> has no predecessor.</summary>
public void RemoveAllLinkAnimations()
{
while (_firstCyclic?.Previous is not null)
RemoveLinkAnimations(1);
}
/// <summary>
/// <c>apricot</c> (0x00524b40; the PDB-verified retail name): trim
/// consumed nodes from the head, bounded by BOTH <c>curr_anim</c>
/// (stop — still live) and <c>first_cyclic</c> (defensive bound —
/// never delete into the cyclic tail). Called after every update (§22).
/// </summary>
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) ──────────────────
/// <summary>
/// Scale the framerate of every node from <c>first_cyclic</c> to the
/// tail. Framerates ONLY (G13) — retail rescales the sequence
/// velocity/omega separately via <c>change_cycle_speed</c>'s
/// <c>subtract_motion</c>/<c>combine_motion</c> composite (R2).
/// </summary>
public void MultiplyCyclicAnimationFramerate(float factor)
{
for (var n = _firstCyclic; n is not null; n = n.Next)
n.Value.MultiplyFramerate(factor);
}
// ── placement + accessors (§15-§17) ─────────────────────────────────
/// <summary><c>set_placement_frame</c> (0x005249b0).</summary>
public void SetPlacementFrame(AnimationFrame? frame, uint id)
{
PlacementFrame = frame;
PlacementFrameId = id;
}
/// <summary><c>get_curr_animframe</c> (0x00524970): the floored current
/// part frame, or the placement frame when no node is active.</summary>
public AnimationFrame? GetCurrAnimframe()
{
if (_currAnim is null)
return PlacementFrame;
return _currAnim.Value.GetPartFrame((int)Math.Floor(FrameNumber));
}
/// <summary><c>get_curr_frame_number</c> (0x005249d0).</summary>
public int GetCurrFrameNumber() => (int)Math.Floor(FrameNumber);
// ── internal cursors for P4 (update_internal operates on nodes) ─────
internal LinkedListNode<AnimSequenceNode>? CurrAnimNode
{
get => _currAnim;
set => _currAnim = value;
}
internal LinkedListNode<AnimSequenceNode>? FirstCyclicNode => _firstCyclic;
internal LinkedList<AnimSequenceNode> AnimList => _animList;
}