acdream/tests/AcDream.Core.Tests/Physics/Motion/InterpretedMotionStateActionFifoTests.cs
Erik 8664959152 feat(R3-W1): retail state completion — action FIFOs, MovementParameters, MotionNode, WeenieError renumber (closes J2, J16-codes)
InterpretedMotionState + the unified RawMotionState (LegacyRawMotionState
folded away) gain retail's action FIFO + ApplyMotion/RemoveMotion, ported
verbatim from the raw named decomp (0x0051e790/0x0051eb60 region, pc
293252-293703) including two genuine retail quirks pinned by tests and
independently re-verified against the raw text before commit:
- RawMotionState::ApplyMotion's RunForward (0x44000007) dead branch — a
  cycle-class apply of literal RunForward writes NOTHING (retail encodes
  run as WalkForward + HoldKey_Run on the raw state; same family as the
  D6 apply_run_to_command early-return).
- InterpretedMotionState::RemoveMotion's exact-match-only TurnRight/
  SideStepRight handling (left variants fall through to the style/
  forward branches).

MovementParameters verbatim (A4 pin): 18 named flags per the absolute
mask table, ctor = 0x1EE0F expansion + distance_to_object 0.6 /
fail_distance FLT_MAX / speed 1 / walk_run_threshhold 15 / hold_key
Invalid — with the two ACE-divergence traps ported RETAIL-side
(can_charge FALSE where ACE defaults true; threshold 15.0 not 1.0).
MotionNode {ContextId, Motion, JumpErrorCode} (acclient.h:53293) — the
pending_motions node W2 consumes.

WeenieError renumbered to retail's numeric semantics (A10 table):
NotGrounded=0x24, GeneralMovementFailure 0x24→0x47, new 0x3f/0x40/0x41
combat-stance rejects + 0x42 chat-emote + 0x45 action-depth; the
airborne jump/action returns corrected 0x48→0x24 per the A10 sweep
(incl. jump_is_allowed's null-physics-obj 0x24-not-8 divergence from
ACE). Codes are local-only — wire untouched.

Outbound packer proof: RawMotionStatePacker unmodified; all 6
golden-byte RawMotionStatePackTests pass unmodified. TS-24 register row
updated (capability landed; outbound wiring still open).

Implemented by a dedicated agent against the W0-pinned spec; quirks,
scope, and suite independently verified. Full suite green: 3,531
(374+425+713+2015+4 pre-existing skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:12:33 +02:00

217 lines
7.4 KiB
C#

using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics.Motion;
/// <summary>
/// R3-W1 — action FIFO discipline + <c>ApplyMotion</c>/<c>RemoveMotion</c>
/// field effects on <see cref="InterpretedMotionState"/> (closes J2). Oracle:
/// docs/research/named-retail/acclient_2013_pseudo_c.txt — verbatim bodies
/// quoted in MotionInterpreter.cs doc comments:
/// <c>InterpretedMotionState::AddAction</c> (0x0051e9e0), <c>RemoveAction</c>
/// (0x0051ead0), <c>GetNumActions</c> (0x0051eb00), <c>ApplyMotion</c>
/// (0x0051ea40), <c>RemoveMotion</c> (0x0051e790).
/// </summary>
public sealed class InterpretedMotionStateActionFifoTests
{
[Fact]
public void Default_HasEmptyActionsAndZeroCount()
{
var ims = InterpretedMotionState.Default();
Assert.Empty(ims.Actions);
Assert.Equal(0u, ims.GetNumActions());
}
// ── AddAction / RemoveAction / GetNumActions FIFO discipline ──────────
[Fact]
public void AddAction_AppendsInOrder_GetNumActionsCounts()
{
var ims = InterpretedMotionState.Default();
ims.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
ims.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
Assert.Equal(2u, ims.GetNumActions());
Assert.Equal((ushort)0x004Bu, ims.Actions[0].Command);
Assert.Equal((ushort)0x0050u, ims.Actions[1].Command);
}
[Fact]
public void RemoveAction_PopsHeadFirst_FifoOrder()
{
var ims = InterpretedMotionState.Default();
ims.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
ims.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
uint first = ims.RemoveAction();
uint second = ims.RemoveAction();
Assert.Equal(0x004Bu, first);
Assert.Equal(0x0050u, second);
Assert.Equal(0u, ims.GetNumActions());
}
[Fact]
public void RemoveAction_Empty_ReturnsZero()
{
var ims = InterpretedMotionState.Default();
Assert.Equal(0u, ims.RemoveAction());
}
[Fact]
public void GetNumActions_DefaultStruct_NoNullRef()
{
// Defensive: a bare default(InterpretedMotionState) (bypassing
// .Default()) must not NRE on GetNumActions/Actions/RemoveAction —
// the lazy-list field starts null.
InterpretedMotionState bare = default;
Assert.Equal(0u, bare.GetNumActions());
Assert.Empty(bare.Actions);
Assert.Equal(0u, bare.RemoveAction());
}
// ── DoMotion's depth-cap gate consumes this directly (documentation) ──
[Fact]
public void GetNumActions_SixQueued_MeetsDoMotionDepthCapThreshold()
{
// DoMotion (0x00528d20 @306159) rejects with 0x45 when an
// action-class motion arrives AND GetNumActions() >= 6. W1 only
// proves the counter is correct; the gate itself lands in W5.
var ims = InterpretedMotionState.Default();
for (uint i = 0; i < 6; i++)
ims.AddAction(0x10000000u + i, 1.0f, i, false);
Assert.Equal(6u, ims.GetNumActions());
Assert.True(ims.GetNumActions() >= 6);
}
// ── ApplyMotion field effects (0x0051ea40) ─────────────────────────────
[Fact]
public void ApplyMotion_TurnRight_SetsTurnCommandAndSpeed()
{
var ims = InterpretedMotionState.Default();
var p = new MovementParameters { Speed = 1.5f };
ims.ApplyMotion(0x6500000du, p);
Assert.Equal(0x6500000du, ims.TurnCommand);
Assert.Equal(1.5f, ims.TurnSpeed);
}
[Fact]
public void ApplyMotion_SideStepRight_SetsSideStepCommandAndSpeed()
{
var ims = InterpretedMotionState.Default();
var p = new MovementParameters { Speed = 1.248f };
ims.ApplyMotion(0x6500000fu, p);
Assert.Equal(0x6500000fu, ims.SideStepCommand);
Assert.Equal(1.248f, ims.SideStepSpeed);
}
[Fact]
public void ApplyMotion_ForwardClassMotion_SetsForwardCommandAndSpeed()
{
// Unlike RawMotionState::ApplyMotion, InterpretedMotionState's
// forward-class branch has NO RunForward exclusion — every
// forward-class id (bit 0x40000000) writes forward_command.
var ims = InterpretedMotionState.Default();
var p = new MovementParameters { Speed = 2.94f };
ims.ApplyMotion(0x44000007u, p); // RunForward
Assert.Equal(0x44000007u, ims.ForwardCommand);
Assert.Equal(2.94f, ims.ForwardSpeed);
}
[Fact]
public void ApplyMotion_StyleClassMotion_ResetsForwardToReady_SetsCurrentStyle()
{
var ims = InterpretedMotionState.Default();
ims.ForwardCommand = 0x45000005u;
var p = new MovementParameters();
ims.ApplyMotion(0x80000042u, p);
Assert.Equal(0x41000003u, ims.ForwardCommand);
Assert.Equal(0x80000042u, ims.CurrentStyle);
}
[Fact]
public void ApplyMotion_ActionClassMotion_AddsAction()
{
var ims = InterpretedMotionState.Default();
var p = new MovementParameters { Speed = 1.0f, ActionStamp = 7u, Autonomous = true };
ims.ApplyMotion(0x1000004Bu, p);
Assert.Equal(1u, ims.GetNumActions());
var a = ims.Actions[0];
Assert.Equal((ushort)0x004Bu, a.Command);
Assert.Equal(1.0f, a.Speed);
Assert.Equal((ushort)7u, a.Stamp);
Assert.True(a.Autonomous);
}
// ── RemoveMotion field effects (0x0051e790) ────────────────────────────
[Fact]
public void RemoveMotion_TurnRightExact_ClearsTurnCommand()
{
var ims = InterpretedMotionState.Default();
ims.TurnCommand = 0x6500000du;
ims.RemoveMotion(0x6500000du);
Assert.Equal(0u, ims.TurnCommand);
}
[Fact]
public void RemoveMotion_TurnLeftExact_DoesNotMatchTurnBranch_FallsThroughToStyleCheck()
{
// Asymmetric vs RawMotionState::RemoveMotion: InterpretedMotionState
// only exact-matches 0x6500000d (TurnRight) for the turn branch, NOT
// 0x6500000e (TurnLeft) — verbatim per the raw decomp.
var ims = InterpretedMotionState.Default();
ims.TurnCommand = 0x6500000eu;
ims.RemoveMotion(0x6500000eu);
// Falls through: bit 0x40000000 clear, arg2 (0x6500000e) is not
// negative and != current_style (0x8000003D default) -> no-op.
Assert.Equal(0x6500000eu, ims.TurnCommand); // untouched
}
[Fact]
public void RemoveMotion_SideStepRightExact_ClearsSideStepCommand()
{
var ims = InterpretedMotionState.Default();
ims.SideStepCommand = 0x6500000fu;
ims.RemoveMotion(0x6500000fu);
Assert.Equal(0u, ims.SideStepCommand);
}
[Fact]
public void RemoveMotion_ForwardClassMotion_MatchingCommand_ResetsToReady()
{
var ims = InterpretedMotionState.Default();
ims.ForwardCommand = 0x45000005u;
ims.ForwardSpeed = 3.0f;
ims.RemoveMotion(0x45000005u);
Assert.Equal(0x41000003u, ims.ForwardCommand);
Assert.Equal(1f, ims.ForwardSpeed);
}
[Fact]
public void RemoveMotion_StyleClassMotion_MatchingCurrentStyle_ResetsToNonCombat()
{
var ims = InterpretedMotionState.Default();
ims.CurrentStyle = 0x80000042u;
ims.RemoveMotion(0x80000042u);
Assert.Equal(0x8000003du, ims.CurrentStyle);
}
}