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:
parent
1371c2a14c
commit
778744bf3e
3 changed files with 592 additions and 1 deletions
266
src/AcDream.Core/Physics/Motion/CSequence.cs
Normal file
266
src/AcDream.Core/Physics/Motion/CSequence.cs
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue