diff --git a/src/AcDream.Core/Physics/Motion/MotionState.cs b/src/AcDream.Core/Physics/Motion/MotionState.cs
new file mode 100644
index 00000000..94c96a58
--- /dev/null
+++ b/src/AcDream.Core/Physics/Motion/MotionState.cs
@@ -0,0 +1,122 @@
+using System.Collections.Generic;
+
+namespace AcDream.Core.Physics.Motion;
+
+///
+/// One entry of a chain — retail's
+/// MotionList 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.
+///
+public sealed class MotionEntry
+{
+ public uint Motion;
+ public float SpeedMod = 1f;
+
+ public MotionEntry(uint motion, float speedMod)
+ {
+ Motion = motion;
+ SpeedMod = speedMod;
+ }
+}
+
+///
+/// R2-Q1 — verbatim port of retail's MotionState (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 CMotionTable::GetObjectSequence,
+/// StopSequenceMotion, re_modify, and
+/// MotionTableManager::AnimationDone read and write.
+///
+public sealed class MotionState
+{
+ /// Current style/stance command word (0 = unset; ctor 0x00525fd0).
+ public uint Style;
+
+ /// Current base substate command word (0 = unset).
+ public uint Substate;
+
+ /// Speed modifier of the base substate (default 1.0).
+ public float SubstateMod = 1f;
+
+ private readonly LinkedList _modifiers = new(); // modifier_head — push-front stack
+ private readonly LinkedList _actions = new(); // action_head/tail — FIFO
+
+ public MotionState()
+ {
+ }
+
+ ///
+ /// Deep copy (retail copy ctor 0x00526790; Q0-pins A4-#5): both chains
+ /// CLONED — re_modify's snapshot is a termination bound, never
+ /// shared state.
+ ///
+ 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));
+ }
+
+ /// Modifier chain in retail order (newest first — push-front).
+ public IEnumerable Modifiers => _modifiers;
+
+ /// Action FIFO in retail order (oldest first).
+ public IEnumerable Actions => _actions;
+
+ /// add_modifier_no_check (0x00525ff0): unconditional
+ /// push-front.
+ public void AddModifierNoCheck(uint motion, float speedMod)
+ => _modifiers.AddFirst(new MotionEntry(motion, speedMod));
+
+ ///
+ /// add_modifier (0x00526340): refuse when the motion is already
+ /// a modifier (caller must stop-then-re-add to refresh) or already IS
+ /// the current base substate.
+ ///
+ 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;
+ }
+
+ /// remove_modifier (0x00526040) — by node identity
+ /// (retail's node+predecessor pair collapses in a managed list).
+ public void RemoveModifier(MotionEntry entry)
+ {
+ _modifiers.Remove(entry);
+ }
+
+ /// clear_modifiers (0x00526070).
+ public void ClearModifiers() => _modifiers.Clear();
+
+ /// add_action (0x005260a0): tail append.
+ public void AddAction(uint motion, float speedMod)
+ => _actions.AddLast(new MotionEntry(motion, speedMod));
+
+ /// remove_action_head (0x00526120): pop the FIFO head,
+ /// returning its motion id (0 when empty).
+ public uint RemoveActionHead()
+ {
+ var head = _actions.First;
+ if (head is null)
+ return 0;
+ _actions.RemoveFirst();
+ return head.Value.Motion;
+ }
+
+ /// clear_actions (0x005260f0).
+ public void ClearActions() => _actions.Clear();
+}
diff --git a/tests/AcDream.Core.Tests/Physics/Motion/MotionStateTests.cs b/tests/AcDream.Core.Tests/Physics/Motion/MotionStateTests.cs
new file mode 100644
index 00000000..41c51f98
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/Motion/MotionStateTests.cs
@@ -0,0 +1,145 @@
+using System.Linq;
+using AcDream.Core.Physics.Motion;
+
+namespace AcDream.Core.Tests.Physics.Motion;
+
+///
+/// R2-Q1 — verbatim MotionState (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).
+///
+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);
+ }
+}