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
|
|
@ -61,7 +61,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 2. Adaptation (AD) — 32 rows
|
||||
## 2. Adaptation (AD) — 34 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -96,6 +96,8 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AD-30 | Cell-march preserves seed landblock id when `TryGetTerrainOrigin` returns false for an outdoor seed (#145 D, 2026-06-22): `BuildCellSetAndPickContaining` returns `currentCellId` verbatim rather than marching via `blockOrigin=(0,0,0)`; retail never encounters this state (cells stored block-local, no streaming-gap concept) | `src/AcDream.Core/Physics/CellTransit.cs:765` | Equivalence argument: "preserve-verbatim when unregistered" is the same contract as `PhysicsEngine.Resolve`'s NO-LANDBLOCK branch; the player's cell stays the last known-correct cell until the landblock's terrain registers — no march, no lbX=0 wire | A body whose seed landblock is genuinely absent for >1 physics tick holds its last-known cell rather than discovering the true containing cell; transient only — corrects the instant terrain registers; an indoor seed is explicitly excluded from the guard (outdoor low < 0x100 gate) | `CObjCell::find_cell_list` + block-local storage (retail has no streaming gap); `TryGetTerrainOrigin` pc path |
|
||||
| AD-31 | Teleport transit covered by a full-screen black FADE (`FadeOverlay` + `TeleportAnimSequencer`) instead of retail's 3D portal-tunnel swirl (2026-06-22, spec C). Opaque black holds through the tunnel states; the world ramps back in on `WorldFadeIn`. | `src/AcDream.App/Rendering/FadeOverlay.cs` + `GameWindow.cs` (TAS transit tick; `_teleportFadeAlpha = ShowTunnel ? 1 : FadeAlpha`) | The fade is a functional cover that hides the (now-fast) destination load + the post-materialization object flood; the TAS state machine + golden timing constants are retail-verbatim — only the tunnel *graphic* is approximated. Sibling to AP-49 (fade-curve). | Visual-only: the transit shows a black cover, not the animated swirl. Retire by porting the `gmSmartBoxUI` 3D tunnel render. | `gmSmartBoxUI::UseTime` 0x004d6e30 (tunnel render, unported); `TELEPORT_ANIM_*` golden constants (spec §2.1) |
|
||||
| AD-32 | Movement-event staleness gate ADOPTS a newer-incarnation instance stamp and applies the event immediately; retail queues the blob for the not-yet-created object (`SmartBox::QueueBlobForObject`, dispatch return 4) and replays it once that incarnation exists (L.2g S1, 2026-07-02) | `src/AcDream.Core/Physics/MotionSequenceGate.cs:105` | acdream has no per-object blob queue; a newer instance seq means the new incarnation's CreateObject is in flight, and that CreateObject re-seeds the same gate (advance-only), so old-incarnation stragglers still drop | A motion event for a new incarnation applies to the OLD body for up to one CreateObject round-trip — brief wrong-cycle flicker on respawn/re-instance if the new incarnation's motion table differs | 0xF74C dispatch pc:357214-357239 (`is_newer(update_times[8], seq)` + `QueueBlobForObject`) |
|
||||
| AD-33 | `CSequence.frame_number` at C# `double` (64-bit); retail is x87 `long double` (80-bit extended) — every frame-boundary comparison ran at extended precision on retail (Phase R1, 2026-07-02) | `src/AcDream.Core/Physics/Motion/CSequence.cs` (`FrameNumber`) | `double` is the widest C# float type; the R1 port removes ACE-style boundary epsilons so comparisons are exact-int against bare boundaries, minimizing ULP sensitivity (ACE's `float` is far worse) | A frame landing within 1 double-ULP of an integer boundary could classify differently than retail's 80-bit compare — sub-frame timing skew at pathological framerate×dt combinations | `acclient.h:30747` (`long double frame_number`) |
|
||||
| AD-34 | Retail's intrusive `DLListBase`/`DLListData` anim-node list is a managed `LinkedList<AnimSequenceNode>`; node identity via `LinkedListNode<>` references (Phase R1, 2026-07-02) | `src/AcDream.Core/Physics/Motion/CSequence.cs` (`_animList`) | Same topology + cursor semantics (curr_anim/first_cyclic are node references); unlink/delete becomes `Remove(node)`; conformance tests pin the surgery state tables | Any retail behavior depending on the −4 pointer adjustment or node memory reuse (none observed in the decomp) would diverge | `acclient.h` DLListBase; `r1-csequence-decomp.md` §0 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
323
tests/AcDream.Core.Tests/Physics/Motion/CSequenceTests.cs
Normal file
323
tests/AcDream.Core.Tests/Physics/Motion/CSequenceTests.cs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R1-P2 — verbatim <c>CSequence</c> container + list surgery (gap-map
|
||||
/// G10/G11/G12/G14/G15/G20). Oracle: r1-csequence-decomp.md §1-§17, §20,
|
||||
/// §24 (ctor 0x005249f0, clear 0x005255b0, clear_animations 0x00524dc0,
|
||||
/// clear_physics 0x00524d50, remove_cyclic_anims 0x00524e40,
|
||||
/// remove_link_animations 0x00524be0, remove_all_link_animations
|
||||
/// 0x00524ca0, apricot 0x00524b40, append_animation 0x00525510,
|
||||
/// set/combine/subtract physics 0x00524880-0x00524900,
|
||||
/// multiply_cyclic_animation_fr 0x00524940, placement family
|
||||
/// 0x00524970-0x005249d0).
|
||||
///
|
||||
/// KEY RETAIL SEMANTICS UNDER TEST:
|
||||
/// - append_animation slides first_cyclic to the JUST-APPENDED node on
|
||||
/// EVERY call (G10) and seeds curr_anim=head + frame_number=starting
|
||||
/// only when curr_anim was null;
|
||||
/// - remove_cyclic_anims snaps a removed curr_anim BACK to the previous
|
||||
/// node at its get_ending_frame() (or 0.0 when none);
|
||||
/// - remove_link_animations snaps a removed curr_anim FORWARD to
|
||||
/// first_cyclic at its get_starting_frame();
|
||||
/// - apricot trims consumed head nodes bounded by curr_anim AND
|
||||
/// first_cyclic;
|
||||
/// - clear (0x005255b0 raw body) resets placement_frame/id too — the
|
||||
/// "2-instruction clear" note in the gap map was wrong, the raw body
|
||||
/// is authority;
|
||||
/// - physics accumulators live on the SEQUENCE (replace/combine/subtract);
|
||||
/// - multiply_cyclic_animation_fr touches node framerates ONLY (G13).
|
||||
/// </summary>
|
||||
public class CSequenceTests
|
||||
{
|
||||
private sealed class MapLoader : IAnimationLoader
|
||||
{
|
||||
private readonly Dictionary<uint, Animation> _map = new();
|
||||
public void Add(uint id, Animation anim) => _map[id] = anim;
|
||||
public Animation? LoadAnimation(uint id) => _map.TryGetValue(id, out var a) ? a : null;
|
||||
}
|
||||
|
||||
private static Animation MakeAnim(int numFrames)
|
||||
{
|
||||
var anim = new Animation();
|
||||
for (int f = 0; f < numFrames; f++)
|
||||
{
|
||||
var pf = new AnimationFrame(1u);
|
||||
pf.Frames.Add(new Frame { Origin = new Vector3(f, 0, 0), Orientation = Quaternion.Identity });
|
||||
anim.PartFrames.Add(pf);
|
||||
}
|
||||
return anim;
|
||||
}
|
||||
|
||||
private static AnimData Ad(uint animId, int low = 0, int high = -1, float framerate = 30f)
|
||||
{
|
||||
QualifiedDataId<Animation> qid = animId;
|
||||
return new AnimData { AnimId = qid, LowFrame = low, HighFrame = high, Framerate = framerate };
|
||||
}
|
||||
|
||||
private static (CSequence seq, MapLoader loader) NewSeq(params (uint id, int frames)[] anims)
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
foreach (var (id, frames) in anims)
|
||||
loader.Add(id, MakeAnim(frames));
|
||||
return (new CSequence(loader), loader);
|
||||
}
|
||||
|
||||
// ── append_animation (G10) ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Append_First_SeedsCurrAnimAndFrameNumber()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10));
|
||||
seq.AppendAnimation(Ad(1u, low: 3));
|
||||
|
||||
Assert.Equal(1, seq.Count);
|
||||
Assert.NotNull(seq.CurrAnim);
|
||||
Assert.Same(seq.CurrAnim, seq.FirstCyclic);
|
||||
Assert.Equal(3.0, seq.FrameNumber); // head.get_starting_frame()
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Append_SlidesFirstCyclicToNewTail_EveryCall()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5), (3u, 4));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.AppendAnimation(Ad(2u));
|
||||
seq.AppendAnimation(Ad(3u));
|
||||
|
||||
Assert.Equal(3, seq.Count);
|
||||
Assert.Equal(4 - 1, seq.FirstCyclic!.HighFrame); // the LAST appended (anim 3, 4 frames)
|
||||
Assert.Equal(10 - 1, seq.CurrAnim!.HighFrame); // curr stays at head (anim 1)
|
||||
Assert.Equal(0.0, seq.FrameNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Append_UnresolvableAnim_Discarded()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10));
|
||||
seq.AppendAnimation(Ad(999u)); // not in loader
|
||||
Assert.Equal(0, seq.Count);
|
||||
Assert.Null(seq.CurrAnim);
|
||||
Assert.False(seq.HasAnims());
|
||||
}
|
||||
|
||||
// ── remove_cyclic_anims (G11) ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RemoveCyclicAnims_CurrOutsideTail_KeepsCurr_FirstCyclicToNewTail()
|
||||
{
|
||||
// A (link) + B (cycle): first_cyclic == B, curr == A (head).
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.AppendAnimation(Ad(2u));
|
||||
|
||||
seq.RemoveCyclicAnims();
|
||||
|
||||
Assert.Equal(1, seq.Count); // B deleted
|
||||
Assert.Equal(9, seq.CurrAnim!.HighFrame); // still A
|
||||
Assert.Same(seq.CurrAnim, seq.FirstCyclic); // first_cyclic = new tail (A)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveCyclicAnims_CurrInsideTail_SnapsBackToPrevAtEndingFrame()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.AppendAnimation(Ad(2u));
|
||||
seq.SetCurrAnimForTest(1); // curr = B (index 1, inside cyclic tail)
|
||||
|
||||
seq.RemoveCyclicAnims();
|
||||
|
||||
Assert.Equal(1, seq.Count);
|
||||
Assert.Equal(9, seq.CurrAnim!.HighFrame); // snapped back to A
|
||||
Assert.Equal(10.0, seq.FrameNumber); // A.get_ending_frame() = high+1 = 10
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveCyclicAnims_SingleNode_EmptiesAndZeroes()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
|
||||
seq.RemoveCyclicAnims();
|
||||
|
||||
Assert.Equal(0, seq.Count);
|
||||
Assert.Null(seq.CurrAnim);
|
||||
Assert.Null(seq.FirstCyclic);
|
||||
Assert.Equal(0.0, seq.FrameNumber);
|
||||
}
|
||||
|
||||
// ── remove_link_animations / remove_all_link_animations (G11) ──────
|
||||
|
||||
[Fact]
|
||||
public void RemoveLinkAnimations_RemovesPredecessorsOfFirstCyclic()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5), (3u, 4));
|
||||
seq.AppendAnimation(Ad(1u)); // A
|
||||
seq.AppendAnimation(Ad(2u)); // B
|
||||
seq.AppendAnimation(Ad(3u)); // C = first_cyclic
|
||||
|
||||
seq.RemoveLinkAnimations(1); // removes B (immediate predecessor)
|
||||
|
||||
Assert.Equal(2, seq.Count);
|
||||
Assert.Equal(9, seq.CurrAnim!.HighFrame); // A untouched (curr was A)
|
||||
Assert.Equal(3, seq.FirstCyclic!.HighFrame); // C untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLinkAnimations_CurrRemoved_SnapsForwardToFirstCyclicStart()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5));
|
||||
seq.AppendAnimation(Ad(1u)); // A (curr)
|
||||
seq.AppendAnimation(Ad(2u)); // B = first_cyclic
|
||||
|
||||
seq.RemoveLinkAnimations(1); // removes A == curr
|
||||
|
||||
Assert.Equal(1, seq.Count);
|
||||
Assert.Same(seq.FirstCyclic, seq.CurrAnim);
|
||||
Assert.Equal(0.0, seq.FrameNumber); // B.get_starting_frame() = low = 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveAllLinkAnimations_RemovesEverythingBeforeFirstCyclic()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5), (3u, 4));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.AppendAnimation(Ad(2u));
|
||||
seq.AppendAnimation(Ad(3u)); // first_cyclic
|
||||
|
||||
seq.RemoveAllLinkAnimations();
|
||||
|
||||
Assert.Equal(1, seq.Count);
|
||||
Assert.Same(seq.FirstCyclic, seq.CurrAnim);
|
||||
Assert.Equal(3, seq.CurrAnim!.HighFrame);
|
||||
}
|
||||
|
||||
// ── apricot (G11/G19) ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Apricot_HeadIsCurr_NoOp()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.AppendAnimation(Ad(2u));
|
||||
|
||||
seq.Apricot();
|
||||
|
||||
Assert.Equal(2, seq.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apricot_TrimsConsumedHeads_StopsAtCurr()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5), (3u, 4));
|
||||
seq.AppendAnimation(Ad(1u)); // A (consumed)
|
||||
seq.AppendAnimation(Ad(2u)); // B (curr)
|
||||
seq.AppendAnimation(Ad(3u)); // C = first_cyclic
|
||||
seq.SetCurrAnimForTest(1); // curr = B
|
||||
|
||||
seq.Apricot();
|
||||
|
||||
Assert.Equal(2, seq.Count); // A trimmed
|
||||
Assert.Equal(4, seq.CurrAnim!.HighFrame); // B still curr
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apricot_BoundedByFirstCyclic_EvenIfCurrBeyond()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5), (3u, 4));
|
||||
seq.AppendAnimation(Ad(1u)); // A
|
||||
seq.AppendAnimation(Ad(2u)); // B
|
||||
seq.AppendAnimation(Ad(3u)); // C = first_cyclic = curr target
|
||||
seq.SetCurrAnimForTest(2); // curr = C
|
||||
|
||||
seq.Apricot();
|
||||
|
||||
Assert.Equal(1, seq.Count); // A and B trimmed; stops at first_cyclic
|
||||
Assert.Equal(3, seq.CurrAnim!.HighFrame);
|
||||
}
|
||||
|
||||
// ── physics accumulators (G12) ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Physics_SetCombineSubtract()
|
||||
{
|
||||
var (seq, _) = NewSeq();
|
||||
seq.SetVelocity(new Vector3(1, 2, 3));
|
||||
seq.SetOmega(new Vector3(0, 0, 1));
|
||||
seq.CombinePhysics(new Vector3(1, 0, 0), new Vector3(0, 0, 0.5f));
|
||||
Assert.Equal(new Vector3(2, 2, 3), seq.Velocity);
|
||||
Assert.Equal(new Vector3(0, 0, 1.5f), seq.Omega);
|
||||
seq.SubtractPhysics(new Vector3(2, 2, 3), new Vector3(0, 0, 1.5f));
|
||||
Assert.Equal(Vector3.Zero, seq.Velocity);
|
||||
Assert.Equal(Vector3.Zero, seq.Omega);
|
||||
}
|
||||
|
||||
// ── multiply_cyclic_animation_fr (G13) ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MultiplyCyclicFramerate_TouchesOnlyCyclicTailFramerates()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10), (2u, 5));
|
||||
seq.AppendAnimation(Ad(1u, framerate: 30f)); // A (link, pre-cyclic after next append)
|
||||
seq.AppendAnimation(Ad(2u, framerate: 30f)); // B = first_cyclic
|
||||
|
||||
seq.MultiplyCyclicAnimationFramerate(2f);
|
||||
|
||||
Assert.Equal(30f, seq.CurrAnim!.Framerate); // A untouched
|
||||
Assert.Equal(60f, seq.FirstCyclic!.Framerate); // B scaled
|
||||
// Velocity/Omega untouched (retail scales those via
|
||||
// subtract/combine_motion in R2, never here).
|
||||
Assert.Equal(Vector3.Zero, seq.Velocity);
|
||||
}
|
||||
|
||||
// ── clear family (G20 corrected) ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Clear_WipesAnimsPhysicsAndPlacement()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.SetVelocity(new Vector3(1, 1, 1));
|
||||
var pf = new AnimationFrame(1u);
|
||||
seq.SetPlacementFrame(pf, 0x65u);
|
||||
|
||||
seq.Clear();
|
||||
|
||||
Assert.Equal(0, seq.Count);
|
||||
Assert.Null(seq.CurrAnim);
|
||||
Assert.Equal(0.0, seq.FrameNumber);
|
||||
Assert.Equal(Vector3.Zero, seq.Velocity);
|
||||
Assert.Null(seq.PlacementFrame); // raw 0x005255b0 resets placement too
|
||||
Assert.Equal(0u, seq.PlacementFrameId);
|
||||
}
|
||||
|
||||
// ── placement + accessors (G14) ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetCurrAnimframe_PlacementFallback_WhenNoCurrAnim()
|
||||
{
|
||||
var (seq, _) = NewSeq();
|
||||
var pf = new AnimationFrame(1u);
|
||||
seq.SetPlacementFrame(pf, 0x65u);
|
||||
Assert.Same(pf, seq.GetCurrAnimframe());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrAnimframe_FlooredFrameLookup()
|
||||
{
|
||||
var (seq, _) = NewSeq((1u, 10));
|
||||
seq.AppendAnimation(Ad(1u));
|
||||
seq.FrameNumber = 2.9;
|
||||
var frame = seq.GetCurrAnimframe();
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal(2f, frame!.Frames[0].Origin.X); // frame index 2
|
||||
|
||||
Assert.Equal(2, seq.GetCurrFrameNumber());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue