feat(R2-Q1): verbatim MotionState (gap H2)
Style/Substate/SubstateMod + the two independent MotionList chains with retail's exact disciplines: modifier PUSH-FRONT stack (add_modifier_no_check 0x00525ff0; add_modifier 0x00526340 refusing duplicates AND the current base substate; remove_modifier by node; clear_modifiers) and action TAIL-APPEND FIFO (add_action 0x005260a0; remove_action_head 0x00526120 returning the popped motion, 0 when empty; clear_actions). Deep-copy ctor clones both chains (Q0-pins A4-#5: re_modify's snapshot is a termination bound, never shared). 11 discipline-table tests. Next: Q2 (CMotionTable + free functions — the GetObjectSequence dispatcher). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
dc54a3e41f
commit
2345da30e9
2 changed files with 267 additions and 0 deletions
122
src/AcDream.Core/Physics/Motion/MotionState.cs
Normal file
122
src/AcDream.Core/Physics/Motion/MotionState.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Physics.Motion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One entry of a <see cref="MotionState"/> chain — retail's
|
||||||
|
/// <c>MotionList</c> node (12 bytes: motion + speed_mod + next;
|
||||||
|
/// acclient.h, r2-motiontable-decomp.md §14). One struct, two independent
|
||||||
|
/// chain disciplines: the modifier chain is a PUSH-FRONT STACK, the action
|
||||||
|
/// chain a TAIL-APPEND FIFO.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MotionEntry
|
||||||
|
{
|
||||||
|
public uint Motion;
|
||||||
|
public float SpeedMod = 1f;
|
||||||
|
|
||||||
|
public MotionEntry(uint motion, float speedMod)
|
||||||
|
{
|
||||||
|
Motion = motion;
|
||||||
|
SpeedMod = speedMod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R2-Q1 — verbatim port of retail's <c>MotionState</c> (acclient.h:31081;
|
||||||
|
/// r2-motiontable-decomp.md §13): the motion-table-facing state block —
|
||||||
|
/// current style / substate / substate_mod plus the modifier stack and the
|
||||||
|
/// action FIFO that <c>CMotionTable::GetObjectSequence</c>,
|
||||||
|
/// <c>StopSequenceMotion</c>, <c>re_modify</c>, and
|
||||||
|
/// <c>MotionTableManager::AnimationDone</c> read and write.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MotionState
|
||||||
|
{
|
||||||
|
/// <summary>Current style/stance command word (0 = unset; ctor 0x00525fd0).</summary>
|
||||||
|
public uint Style;
|
||||||
|
|
||||||
|
/// <summary>Current base substate command word (0 = unset).</summary>
|
||||||
|
public uint Substate;
|
||||||
|
|
||||||
|
/// <summary>Speed modifier of the base substate (default 1.0).</summary>
|
||||||
|
public float SubstateMod = 1f;
|
||||||
|
|
||||||
|
private readonly LinkedList<MotionEntry> _modifiers = new(); // modifier_head — push-front stack
|
||||||
|
private readonly LinkedList<MotionEntry> _actions = new(); // action_head/tail — FIFO
|
||||||
|
|
||||||
|
public MotionState()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deep copy (retail copy ctor 0x00526790; Q0-pins A4-#5): both chains
|
||||||
|
/// CLONED — <c>re_modify</c>'s snapshot is a termination bound, never
|
||||||
|
/// shared state.
|
||||||
|
/// </summary>
|
||||||
|
public MotionState(MotionState other)
|
||||||
|
{
|
||||||
|
Style = other.Style;
|
||||||
|
Substate = other.Substate;
|
||||||
|
SubstateMod = other.SubstateMod;
|
||||||
|
foreach (var m in other._modifiers)
|
||||||
|
_modifiers.AddLast(new MotionEntry(m.Motion, m.SpeedMod));
|
||||||
|
foreach (var a in other._actions)
|
||||||
|
_actions.AddLast(new MotionEntry(a.Motion, a.SpeedMod));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Modifier chain in retail order (newest first — push-front).</summary>
|
||||||
|
public IEnumerable<MotionEntry> Modifiers => _modifiers;
|
||||||
|
|
||||||
|
/// <summary>Action FIFO in retail order (oldest first).</summary>
|
||||||
|
public IEnumerable<MotionEntry> Actions => _actions;
|
||||||
|
|
||||||
|
/// <summary><c>add_modifier_no_check</c> (0x00525ff0): unconditional
|
||||||
|
/// push-front.</summary>
|
||||||
|
public void AddModifierNoCheck(uint motion, float speedMod)
|
||||||
|
=> _modifiers.AddFirst(new MotionEntry(motion, speedMod));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>add_modifier</c> (0x00526340): refuse when the motion is already
|
||||||
|
/// a modifier (caller must stop-then-re-add to refresh) or already IS
|
||||||
|
/// the current base substate.
|
||||||
|
/// </summary>
|
||||||
|
public bool AddModifier(uint motion, float speedMod)
|
||||||
|
{
|
||||||
|
for (var n = _modifiers.First; n is not null; n = n.Next)
|
||||||
|
if (n.Value.Motion == motion)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Substate == motion)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
AddModifierNoCheck(motion, speedMod);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>remove_modifier</c> (0x00526040) — by node identity
|
||||||
|
/// (retail's node+predecessor pair collapses in a managed list).</summary>
|
||||||
|
public void RemoveModifier(MotionEntry entry)
|
||||||
|
{
|
||||||
|
_modifiers.Remove(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>clear_modifiers</c> (0x00526070).</summary>
|
||||||
|
public void ClearModifiers() => _modifiers.Clear();
|
||||||
|
|
||||||
|
/// <summary><c>add_action</c> (0x005260a0): tail append.</summary>
|
||||||
|
public void AddAction(uint motion, float speedMod)
|
||||||
|
=> _actions.AddLast(new MotionEntry(motion, speedMod));
|
||||||
|
|
||||||
|
/// <summary><c>remove_action_head</c> (0x00526120): pop the FIFO head,
|
||||||
|
/// returning its motion id (0 when empty).</summary>
|
||||||
|
public uint RemoveActionHead()
|
||||||
|
{
|
||||||
|
var head = _actions.First;
|
||||||
|
if (head is null)
|
||||||
|
return 0;
|
||||||
|
_actions.RemoveFirst();
|
||||||
|
return head.Value.Motion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>clear_actions</c> (0x005260f0).</summary>
|
||||||
|
public void ClearActions() => _actions.Clear();
|
||||||
|
}
|
||||||
145
tests/AcDream.Core.Tests/Physics/Motion/MotionStateTests.cs
Normal file
145
tests/AcDream.Core.Tests/Physics/Motion/MotionStateTests.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
using System.Linq;
|
||||||
|
using AcDream.Core.Physics.Motion;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics.Motion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R2-Q1 — verbatim <c>MotionState</c> (gap H2). Oracle:
|
||||||
|
/// r2-motiontable-decomp.md §13 (ctor 0x00525fd0, add_modifier_no_check
|
||||||
|
/// 0x00525ff0, add_modifier 0x00526340, remove_modifier 0x00526040,
|
||||||
|
/// clear_modifiers 0x00526070, add_action 0x005260a0, clear_actions
|
||||||
|
/// 0x005260f0, remove_action_head 0x00526120) + §14 (one node struct,
|
||||||
|
/// two independent chains: modifier PUSH-FRONT STACK vs action TAIL-APPEND
|
||||||
|
/// FIFO) + Q0-pins A4-#5 (deep-copy ctor clones both chains — re_modify's
|
||||||
|
/// snapshot is a termination bound, never shared state).
|
||||||
|
/// </summary>
|
||||||
|
public class MotionStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Defaults_MatchRetailCtor()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
Assert.Equal(0u, ms.Style);
|
||||||
|
Assert.Equal(0u, ms.Substate);
|
||||||
|
Assert.Equal(1f, ms.SubstateMod);
|
||||||
|
Assert.Empty(ms.Modifiers);
|
||||||
|
Assert.Empty(ms.Actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Modifiers_ArePushFrontStack()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
ms.AddModifierNoCheck(0x0Du, 1.0f);
|
||||||
|
ms.AddModifierNoCheck(0x0Fu, 1.5f);
|
||||||
|
|
||||||
|
// Newest first — retail pushes onto modifier_head.
|
||||||
|
Assert.Equal(new uint[] { 0x0Fu, 0x0Du }, ms.Modifiers.Select(m => m.Motion).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Actions_AreTailAppendFifo()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
ms.AddAction(0x62u, 1.0f);
|
||||||
|
ms.AddAction(0x63u, 1.25f);
|
||||||
|
|
||||||
|
Assert.Equal(new uint[] { 0x62u, 0x63u }, ms.Actions.Select(a => a.Motion).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddModifier_RejectsDuplicate()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
Assert.True(ms.AddModifier(0x0Du, 1.0f));
|
||||||
|
Assert.False(ms.AddModifier(0x0Du, 2.0f)); // already present → caller must stop-then-re-add
|
||||||
|
Assert.Single(ms.Modifiers);
|
||||||
|
Assert.Equal(1.0f, ms.Modifiers.First().SpeedMod); // original untouched
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddModifier_RefusesCurrentSubstate()
|
||||||
|
{
|
||||||
|
var ms = new MotionState { Substate = 0x45000005u };
|
||||||
|
Assert.False(ms.AddModifier(0x45000005u, 1.0f));
|
||||||
|
Assert.Empty(ms.Modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddModifierNoCheck_SkipsBothGuards()
|
||||||
|
{
|
||||||
|
var ms = new MotionState { Substate = 0x45000005u };
|
||||||
|
ms.AddModifierNoCheck(0x45000005u, 1.0f);
|
||||||
|
ms.AddModifierNoCheck(0x45000005u, 2.0f); // duplicate allowed too
|
||||||
|
Assert.Equal(2, ms.Modifiers.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveModifier_ByNodeIdentity()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
ms.AddModifierNoCheck(0x0Du, 1.0f);
|
||||||
|
ms.AddModifierNoCheck(0x0Fu, 1.5f);
|
||||||
|
var target = ms.Modifiers.First(m => m.Motion == 0x0Du);
|
||||||
|
|
||||||
|
ms.RemoveModifier(target);
|
||||||
|
|
||||||
|
Assert.Single(ms.Modifiers);
|
||||||
|
Assert.Equal(0x0Fu, ms.Modifiers.First().Motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveActionHead_PopsFifo_ReturnsMotion_ZeroWhenEmpty()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
ms.AddAction(0x62u, 1f);
|
||||||
|
ms.AddAction(0x63u, 1f);
|
||||||
|
|
||||||
|
Assert.Equal(0x62u, ms.RemoveActionHead());
|
||||||
|
Assert.Equal(0x63u, ms.RemoveActionHead());
|
||||||
|
Assert.Equal(0u, ms.RemoveActionHead()); // empty → 0 (retail returns 0)
|
||||||
|
Assert.Empty(ms.Actions);
|
||||||
|
|
||||||
|
// Tail cleared with the last pop — a fresh append works normally.
|
||||||
|
ms.AddAction(0x64u, 1f);
|
||||||
|
Assert.Equal(new uint[] { 0x64u }, ms.Actions.Select(a => a.Motion).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearModifiers_And_ClearActions_AreIndependentChains()
|
||||||
|
{
|
||||||
|
var ms = new MotionState();
|
||||||
|
ms.AddModifierNoCheck(0x0Du, 1f);
|
||||||
|
ms.AddAction(0x62u, 1f);
|
||||||
|
|
||||||
|
ms.ClearModifiers();
|
||||||
|
Assert.Empty(ms.Modifiers);
|
||||||
|
Assert.Single(ms.Actions);
|
||||||
|
|
||||||
|
ms.ClearActions();
|
||||||
|
Assert.Empty(ms.Actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeepCopy_ClonesChains_NoSharedState()
|
||||||
|
{
|
||||||
|
// A4-#5: re_modify's snapshot is a DEEP copy used as a termination
|
||||||
|
// bound — mutating the original must not touch the snapshot.
|
||||||
|
var ms = new MotionState { Style = 0x8000003Du, Substate = 0x44000007u, SubstateMod = 2.85f };
|
||||||
|
ms.AddModifierNoCheck(0x0Du, 1.5f);
|
||||||
|
ms.AddAction(0x62u, 1f);
|
||||||
|
|
||||||
|
var snap = new MotionState(ms);
|
||||||
|
|
||||||
|
ms.ClearModifiers();
|
||||||
|
ms.RemoveActionHead();
|
||||||
|
ms.Substate = 0x41000003u;
|
||||||
|
|
||||||
|
Assert.Equal(0x8000003Du, snap.Style);
|
||||||
|
Assert.Equal(0x44000007u, snap.Substate);
|
||||||
|
Assert.Equal(2.85f, snap.SubstateMod);
|
||||||
|
Assert.Single(snap.Modifiers);
|
||||||
|
Assert.Equal(0x0Du, snap.Modifiers.First().Motion);
|
||||||
|
Assert.Single(snap.Actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue