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>
This commit is contained in:
Erik 2026-07-02 22:12:33 +02:00
parent 220927d350
commit 8664959152
11 changed files with 1483 additions and 117 deletions

View file

@ -0,0 +1,217 @@
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);
}
}

View file

@ -0,0 +1,33 @@
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics.Motion;
/// <summary>
/// R3-W1 — <c>CMotionInterp::MotionNode</c> shape pin. Oracle:
/// acclient.h:53293 (struct #5857). W2 consumes this as the
/// <c>pending_motions</c> element type; W1 only ports the shape.
/// </summary>
public sealed class MotionNodeTests
{
[Fact]
public void Ctor_StoresAllThreeFields()
{
var node = new MotionNode(ContextId: 7u, Motion: 0x41000003u, JumpErrorCode: 0x48u);
Assert.Equal(7u, node.ContextId);
Assert.Equal(0x41000003u, node.Motion);
Assert.Equal(0x48u, node.JumpErrorCode);
}
[Fact]
public void Equality_IsValueBased()
{
var a = new MotionNode(1u, 2u, 3u);
var b = new MotionNode(1u, 2u, 3u);
var c = new MotionNode(1u, 2u, 4u);
Assert.Equal(a, b);
Assert.NotEqual(a, c);
}
}

View file

@ -0,0 +1,112 @@
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics.Motion;
/// <summary>
/// R3-W1 — <c>MovementParameters</c> ctor-default pins. Oracle:
/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A4 (bitfield 0x1EE0F
/// expansion) + r3-motioninterp-decomp.md §0 (scalar ctor, raw 300510-300534,
/// 0x00524380). Every flag asserted individually against the retail
/// 0x1EE0F default so a future bit-numbering slip fails loudly, plus the two
/// ACE-divergence traps (CanCharge, WalkRunThreshhold) get dedicated tests.
/// </summary>
public sealed class MovementParametersTests
{
[Fact]
public void Ctor_SetFlags_MatchRetail0x1EE0FExpansion()
{
var p = new MovementParameters();
// Bits SET in 0x1EE0F: 0x1,0x2,0x4,0x8,0x200,0x400,0x800,0x2000,0x4000,0x8000,0x10000
Assert.True(p.CanWalk); // 0x1
Assert.True(p.CanRun); // 0x2
Assert.True(p.CanSidestep); // 0x4
Assert.True(p.CanWalkBackwards); // 0x8
Assert.True(p.MoveTowards); // 0x200
Assert.True(p.UseSpheres); // 0x400
Assert.True(p.SetHoldKey); // 0x800
Assert.True(p.ModifyRawState); // 0x2000
Assert.True(p.ModifyInterpretedState); // 0x4000
Assert.True(p.CancelMoveTo); // 0x8000
Assert.True(p.StopCompletelyFlag); // 0x10000
}
[Fact]
public void Ctor_ClearFlags_MatchRetail0x1EE0FExpansion()
{
var p = new MovementParameters();
// Bits CLEAR in 0x1EE0F: 0x10,0x20,0x40,0x80,0x100,0x1000,0x20000
Assert.False(p.CanCharge); // 0x10 — ACE-divergence trap
Assert.False(p.FailWalk); // 0x20
Assert.False(p.UseFinalHeading); // 0x40
Assert.False(p.Sticky); // 0x80
Assert.False(p.MoveAway); // 0x100
Assert.False(p.Autonomous); // 0x1000
Assert.False(p.DisableJumpDuringLink); // 0x20000
}
[Fact]
public void Ctor_Bitfield_ReconstitutesExactly0x1EE0F()
{
var p = new MovementParameters();
uint bitfield = 0;
if (p.CanWalk) bitfield |= 0x1;
if (p.CanRun) bitfield |= 0x2;
if (p.CanSidestep) bitfield |= 0x4;
if (p.CanWalkBackwards) bitfield |= 0x8;
if (p.CanCharge) bitfield |= 0x10;
if (p.FailWalk) bitfield |= 0x20;
if (p.UseFinalHeading) bitfield |= 0x40;
if (p.Sticky) bitfield |= 0x80;
if (p.MoveAway) bitfield |= 0x100;
if (p.MoveTowards) bitfield |= 0x200;
if (p.UseSpheres) bitfield |= 0x400;
if (p.SetHoldKey) bitfield |= 0x800;
if (p.Autonomous) bitfield |= 0x1000;
if (p.ModifyRawState) bitfield |= 0x2000;
if (p.ModifyInterpretedState) bitfield |= 0x4000;
if (p.CancelMoveTo) bitfield |= 0x8000;
if (p.StopCompletelyFlag) bitfield |= 0x10000;
if (p.DisableJumpDuringLink) bitfield |= 0x20000;
Assert.Equal(0x1EE0Fu, bitfield);
}
[Fact]
public void Ctor_ScalarDefaults_MatchRetail()
{
var p = new MovementParameters();
Assert.Equal(0f, p.MinDistance);
Assert.Equal(0.6f, p.DistanceToObject);
Assert.Equal(float.MaxValue, p.FailDistance);
Assert.Equal(0f, p.DesiredHeading);
Assert.Equal(1f, p.Speed);
// ACE-divergence trap: retail is 15.0, NOT ACE's 1.0.
Assert.Equal(15f, p.WalkRunThreshhold);
Assert.Equal(0u, p.ContextId);
Assert.Equal(HoldKey.Invalid, p.HoldKeyToApply);
Assert.Equal(0u, p.ActionStamp);
}
[Fact]
public void Ctor_CanCharge_DefaultsFalse_NotAceTrue()
{
// Dedicated regression test for the single highest-risk ACE trap:
// ACE's MovementParameters.cs:58 sets CanCharge = true at construction.
// Retail's 0x1EE0F has bit 0x10 CLEAR.
var p = new MovementParameters();
Assert.False(p.CanCharge);
}
[Fact]
public void Ctor_WalkRunThreshhold_Is15_NotAce1()
{
var p = new MovementParameters();
Assert.Equal(15f, p.WalkRunThreshhold);
}
}

View file

@ -0,0 +1,244 @@
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="RawMotionState"/> (closes J2). Oracle:
/// docs/research/named-retail/acclient_2013_pseudo_c.txt — verbatim bodies
/// quoted in RawMotionState.cs doc comments:
/// <c>RawMotionState::AddAction</c> (0x0051e840), <c>RemoveAction</c>
/// (0x0051e8a0), <c>ApplyMotion</c> (0x0051eb60), <c>RemoveMotion</c>
/// (0x0051e6e0).
/// </summary>
public sealed class RawMotionStateActionFifoTests
{
// ── AddAction / RemoveAction / GetNumActions FIFO discipline ──────────
[Fact]
public void AddAction_AppendsInOrder()
{
var raw = new RawMotionState();
raw.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
raw.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
Assert.Equal(2, raw.Actions.Count);
Assert.Equal((ushort)0x004Bu, raw.Actions[0].Command); // widened to ushort on wire, verified below
Assert.Equal((ushort)0x0050u, raw.Actions[1].Command);
}
[Fact]
public void RemoveAction_PopsHeadFirst_FifoOrder()
{
var raw = new RawMotionState();
raw.AddAction(0x1000004Bu, 1.0f, 1, autonomous: false);
raw.AddAction(0x10000050u, 1.5f, 2, autonomous: true);
uint first = raw.RemoveAction();
uint second = raw.RemoveAction();
Assert.Equal(0x004Bu, first); // head popped first (FIFO)
Assert.Equal(0x0050u, second);
Assert.Empty(raw.Actions);
}
[Fact]
public void RemoveAction_Empty_ReturnsZero()
{
var raw = new RawMotionState();
Assert.Equal(0u, raw.RemoveAction());
}
[Fact]
public void AddAction_StoresSpeedStampAutonomous()
{
var raw = new RawMotionState();
raw.AddAction(0x1000004Bu, speed: 2.5f, actionStamp: 0x7FFFu, autonomous: true);
var a = raw.Actions[0];
Assert.Equal(2.5f, a.Speed);
Assert.Equal((ushort)0x7FFFu, a.Stamp);
Assert.True(a.Autonomous);
}
// ── ApplyMotion field effects (0x0051eb60) ─────────────────────────────
[Fact]
public void ApplyMotion_TurnRight_SetsTurnCommandAndSpeed_HonorsSetHoldKeyBit()
{
var raw = new RawMotionState();
var p = new MovementParameters { Speed = 1.5f, SetHoldKey = true };
raw.ApplyMotion(0x6500000du, p); // TurnRight
Assert.Equal(0x6500000du, raw.TurnCommand);
Assert.Equal(1.5f, raw.TurnSpeed);
Assert.Equal(HoldKey.Invalid, raw.TurnHoldKey); // SetHoldKey bit set -> Invalid
}
[Fact]
public void ApplyMotion_TurnRight_SetHoldKeyClear_UsesHoldKeyToApply()
{
var raw = new RawMotionState();
var p = new MovementParameters { Speed = 1.5f, SetHoldKey = false, HoldKeyToApply = HoldKey.Run };
raw.ApplyMotion(0x6500000du, p);
Assert.Equal(HoldKey.Run, raw.TurnHoldKey);
}
[Fact]
public void ApplyMotion_SideStepRight_SetsSidestepCommandAndSpeed()
{
var raw = new RawMotionState();
var p = new MovementParameters { Speed = 1.248f, SetHoldKey = true };
raw.ApplyMotion(0x6500000fu, p); // SideStepRight
Assert.Equal(0x6500000fu, raw.SidestepCommand);
Assert.Equal(1.248f, raw.SidestepSpeed);
Assert.Equal(HoldKey.Invalid, raw.SidestepHoldKey);
}
[Fact]
public void ApplyMotion_ForwardClassMotion_SetsForwardCommandAndSpeed()
{
// WalkForward = 0x45000005 has bit 0x40000000 set (forward-class)
// and is NOT 0x44000007 (RunForward) -> the write branch fires.
var raw = new RawMotionState();
var p = new MovementParameters { Speed = 1.0f, SetHoldKey = true };
raw.ApplyMotion(0x45000005u, p);
Assert.Equal(0x45000005u, raw.ForwardCommand);
Assert.Equal(1.0f, raw.ForwardSpeed);
Assert.Equal(HoldKey.Invalid, raw.ForwardHoldKey);
}
[Fact]
public void ApplyMotion_RunForwardExactId_ForwardClassButExcluded_NoWrite()
{
// Verbatim retail quirk (0x0051eb60): arg2 == 0x44000007 (RunForward)
// with the forward-class bit (0x40000000) set falls through BOTH
// inner branches — no field write occurs. Port verbatim, not fixed.
var raw = new RawMotionState();
var before = raw.ForwardCommand;
var p = new MovementParameters { Speed = 3.0f };
raw.ApplyMotion(0x44000007u, p);
Assert.Equal(before, raw.ForwardCommand); // untouched
Assert.Equal(1.0f, raw.ForwardSpeed); // untouched (still ctor default)
}
[Fact]
public void ApplyMotion_StyleClassMotion_SetsCurrentStyleAndResetsForwardToReady()
{
// High bit set (>= 0x80000000) and current_style differs -> style branch.
var raw = new RawMotionState { ForwardCommand = 0x45000005u };
var p = new MovementParameters();
raw.ApplyMotion(0x80000042u, p);
Assert.Equal(0x41000003u, raw.ForwardCommand); // reset to Ready
Assert.Equal(0x80000042u, raw.CurrentStyle);
}
[Fact]
public void ApplyMotion_StyleClassMotion_SameAsCurrentStyle_NoOp()
{
var raw = new RawMotionState { CurrentStyle = 0x80000042u, ForwardCommand = 0x45000005u };
var p = new MovementParameters();
raw.ApplyMotion(0x80000042u, p);
// current_style already equals arg2 -> the style branch's condition
// (current_style != arg2) is false, so forward_command is untouched.
Assert.Equal(0x45000005u, raw.ForwardCommand);
Assert.Equal(0x80000042u, raw.CurrentStyle);
}
[Fact]
public void ApplyMotion_ActionClassMotion_AddsAction()
{
// Outside turn/sidestep range, bit 0x40000000 clear, arg2 >= 0
// (not style-class), bit 0x10000000 set -> AddAction.
var raw = new RawMotionState();
var p = new MovementParameters { Speed = 1.0f, ActionStamp = 42u, Autonomous = true };
raw.ApplyMotion(0x1000004Bu, p); // Jumpup action id
Assert.Single(raw.Actions);
var a = raw.Actions[0];
Assert.Equal((ushort)0x004Bu, a.Command);
Assert.Equal(1.0f, a.Speed);
Assert.Equal((ushort)42u, a.Stamp);
Assert.True(a.Autonomous);
}
// ── RemoveMotion field effects (0x0051e6e0) ────────────────────────────
[Fact]
public void RemoveMotion_TurnRange_ClearsTurnCommand()
{
var raw = new RawMotionState { TurnCommand = 0x6500000du };
raw.RemoveMotion(0x6500000du);
Assert.Equal(0u, raw.TurnCommand);
}
[Fact]
public void RemoveMotion_TurnLeftRange_ClearsTurnCommand()
{
var raw = new RawMotionState { TurnCommand = 0x6500000eu };
raw.RemoveMotion(0x6500000eu);
Assert.Equal(0u, raw.TurnCommand);
}
[Fact]
public void RemoveMotion_SidestepRange_ClearsSidestepCommand()
{
var raw = new RawMotionState { SidestepCommand = 0x6500000fu };
raw.RemoveMotion(0x6500000fu);
Assert.Equal(0u, raw.SidestepCommand);
}
[Fact]
public void RemoveMotion_ForwardClassMotion_MatchingCommand_ResetsToReady()
{
var raw = new RawMotionState { ForwardCommand = 0x45000005u, ForwardSpeed = 3.0f };
raw.RemoveMotion(0x45000005u);
Assert.Equal(0x41000003u, raw.ForwardCommand);
Assert.Equal(1f, raw.ForwardSpeed);
}
[Fact]
public void RemoveMotion_ForwardClassMotion_NonMatchingCommand_NoOp()
{
var raw = new RawMotionState { ForwardCommand = 0x45000005u, ForwardSpeed = 3.0f };
raw.RemoveMotion(0x44000007u); // different forward-class id
Assert.Equal(0x45000005u, raw.ForwardCommand); // untouched
Assert.Equal(3.0f, raw.ForwardSpeed);
}
[Fact]
public void RemoveMotion_StyleClassMotion_MatchingCurrentStyle_ResetsToNonCombat()
{
var raw = new RawMotionState { CurrentStyle = 0x80000042u };
raw.RemoveMotion(0x80000042u);
Assert.Equal(0x8000003du, raw.CurrentStyle); // reset to NonCombat
}
[Fact]
public void RemoveMotion_StyleClassMotion_NonMatchingCurrentStyle_NoOp()
{
var raw = new RawMotionState { CurrentStyle = 0x80000042u };
raw.RemoveMotion(0x80000099u); // different style id
Assert.Equal(0x80000042u, raw.CurrentStyle); // untouched
}
}

View file

@ -176,7 +176,7 @@ public sealed class MotionInterpreterTests
{
var interp = MakeInterp();
interp.RawState.ForwardCommand = MotionCommand.WalkForward;
interp.RawState.SideStepCommand = MotionCommand.SideStepRight;
interp.RawState.SidestepCommand = MotionCommand.SideStepRight;
interp.RawState.TurnCommand = MotionCommand.TurnRight;
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
interp.InterpretedState.TurnCommand = MotionCommand.TurnLeft;
@ -186,7 +186,7 @@ public sealed class MotionInterpreterTests
Assert.Equal(WeenieError.None, result);
Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand);
Assert.Equal(1.0f, interp.RawState.ForwardSpeed, precision: 5);
Assert.Equal(0u, interp.RawState.SideStepCommand);
Assert.Equal(0u, interp.RawState.SidestepCommand);
Assert.Equal(0u, interp.RawState.TurnCommand);
Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand);
Assert.Equal(1.0f, interp.InterpretedState.ForwardSpeed, precision: 5);
@ -440,14 +440,16 @@ public sealed class MotionInterpreterTests
}
[Fact]
public void Jump_Airborne_ReturnsYouCantJumpWhileInTheAir()
public void Jump_Airborne_ReturnsNotGrounded()
{
// R3-W1 renumber (A10): airborne is 0x24 (NotGrounded), not 0x48
// (0x48 is now reserved for the motion_allows_jump blocklist).
var body = MakeAirborne();
var interp = MakeInterp(body);
var result = interp.jump(0.5f);
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
Assert.Equal(WeenieError.NotGrounded, result);
}
[Fact]
@ -586,13 +588,13 @@ public sealed class MotionInterpreterTests
}
[Fact]
public void JumpIsAllowed_Airborne_ReturnsYouCantJumpWhileInTheAir()
public void JumpIsAllowed_Airborne_ReturnsNotGrounded()
{
var interp = MakeInterp(MakeAirborne());
var result = interp.jump_is_allowed(0.5f, 0);
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
Assert.Equal(WeenieError.NotGrounded, result);
}
[Fact]
@ -607,7 +609,7 @@ public sealed class MotionInterpreterTests
}
[Fact]
public void JumpIsAllowed_NoGravityFlag_ReturnsYouCantJumpWhileInTheAir()
public void JumpIsAllowed_NoGravityFlag_ReturnsNotGrounded()
{
var body = new PhysicsBody
{
@ -619,17 +621,20 @@ public sealed class MotionInterpreterTests
// No gravity → must be airborne-style
var result = interp.jump_is_allowed(0.5f, 0);
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
Assert.Equal(WeenieError.NotGrounded, result);
}
[Fact]
public void JumpIsAllowed_NullPhysicsObj_ReturnsGeneralFailure()
public void JumpIsAllowed_NullPhysicsObj_ReturnsNotGrounded()
{
// A10 note: jump_is_allowed returns 0x24 (NotGrounded), NOT 8, when
// physics_obj == null — the "8 = no physics obj" convention that
// holds everywhere else in CMotionInterp does not hold here.
var interp = new MotionInterpreter();
var result = interp.jump_is_allowed(0.5f, 0);
Assert.Equal(WeenieError.GeneralMovementFailure, result);
Assert.Equal(WeenieError.NotGrounded, result);
}
// =========================================================================

View file

@ -0,0 +1,81 @@
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// R3-W1 — pins <see cref="WeenieError"/>'s numeric values against the
/// definitive retail table in
/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A10 (an exhaustive
/// sweep of every <c>return &lt;code&gt;</c> site across raw 304908-306277 +
/// 300150-300540: 19 return sites + 1 store site, all attributed). These
/// codes are local-only (never on the wire), so the renumber from the
/// pre-R3 shuffled names is a safe rename — this test is the single
/// source of truth that a future edit can't silently un-pin.
/// </summary>
public sealed class WeenieErrorCodeTableTests
{
[Fact]
public void None_Is0x00()
=> Assert.Equal(0x00u, (uint)WeenieError.None);
[Fact]
public void NoPhysicsObject_Is0x08()
=> Assert.Equal(0x08u, (uint)WeenieError.NoPhysicsObject);
[Fact]
public void NotGrounded_Is0x24()
=> Assert.Equal(0x24u, (uint)WeenieError.NotGrounded);
[Fact]
public void CrouchInCombatStance_Is0x3f()
=> Assert.Equal(0x3fu, (uint)WeenieError.CrouchInCombatStance);
[Fact]
public void SitInCombatStance_Is0x40()
=> Assert.Equal(0x40u, (uint)WeenieError.SitInCombatStance);
[Fact]
public void SleepInCombatStance_Is0x41()
=> Assert.Equal(0x41u, (uint)WeenieError.SleepInCombatStance);
[Fact]
public void ChatEmoteOutsideNonCombat_Is0x42()
=> Assert.Equal(0x42u, (uint)WeenieError.ChatEmoteOutsideNonCombat);
[Fact]
public void ActionDepthExceeded_Is0x45()
=> Assert.Equal(0x45u, (uint)WeenieError.ActionDepthExceeded);
[Fact]
public void GeneralMovementFailure_Is0x47()
=> Assert.Equal(0x47u, (uint)WeenieError.GeneralMovementFailure);
[Fact]
public void YouCantJumpFromThisPosition_Is0x48()
=> Assert.Equal(0x48u, (uint)WeenieError.YouCantJumpFromThisPosition);
[Fact]
public void CantJumpLoadedDown_Is0x49()
=> Assert.Equal(0x49u, (uint)WeenieError.CantJumpLoadedDown);
/// <summary>
/// Every code in the A10 table in one pass — guards against a
/// future partial edit desyncing an individual test above from the
/// enum declaration.
/// </summary>
[Theory]
[InlineData(WeenieError.None, 0x00u)]
[InlineData(WeenieError.NoPhysicsObject, 0x08u)]
[InlineData(WeenieError.NotGrounded, 0x24u)]
[InlineData(WeenieError.CrouchInCombatStance, 0x3fu)]
[InlineData(WeenieError.SitInCombatStance, 0x40u)]
[InlineData(WeenieError.SleepInCombatStance, 0x41u)]
[InlineData(WeenieError.ChatEmoteOutsideNonCombat, 0x42u)]
[InlineData(WeenieError.ActionDepthExceeded, 0x45u)]
[InlineData(WeenieError.GeneralMovementFailure, 0x47u)]
[InlineData(WeenieError.YouCantJumpFromThisPosition, 0x48u)]
[InlineData(WeenieError.CantJumpLoadedDown, 0x49u)]
public void A10Table_EveryCode_MatchesRetailNumericValue(WeenieError code, uint expected)
=> Assert.Equal(expected, (uint)code);
}