feat(R4-V1): command-selection family + state widening (closes M2-mechanics, M11, M12, M15)

MovementParameters gains the verbatim selection family:
GetCommand 0x0052aa00 (the walk-vs-run cascade INCLUDING the CanCharge
0x10 fast-path ACE dropped - retail's default can_charge=false + the
fast-path present, the A13+A15 canceling-pair trap avoided; inclusive
threshold edge per the raw), TowardsAndAway, GetDesiredHeading per the
live Ghidra decompile (fwd-towards 0 / fwd-away 180 / back-towards 180
/ back-away 0), FromWire/FromWireTurnTo (UnPackNet semantics, all 18 A4
masks round-tripped).

New MoveToMath: HeadingDiff per the live Ghidra decompile of 0x00528fb0
(the 360-diff NOT-TurnRight mirror + F_EPSILON 0.000199999995f - the
BN "arg unused" artifact corrected), HeadingGreater (the visible
TurnRight idiom), PositionHeading/Get/SetHeading reusing the codebase's
single yaw-heading convention (P5), CylinderDistance (PDB arg order;
planar-minus-radii shape documented as the interpretation - the raw's
x87 body is garbled; seam noted).

MovementType gains Invalid + retail 6/7/8/9; MovementStruct widened
(ObjectId/TopLevelId/Pos/Radius/Height/Params, additive); WeenieError
+= 0x0B/0x36/0x37/0x38/0x3D with retail-meaning doc comments.

148 new conformance tests. Full suite: 3,860 passed.

Implemented by a dedicated agent against the V0-pinned spec; scope +
suite independently verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 11:13:15 +02:00
parent 386b1ce550
commit e0d2492cbb
14 changed files with 1973 additions and 4 deletions

View file

@ -0,0 +1,340 @@
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics.Motion;
/// <summary>
/// R4-V1 — <c>MovementParameters::get_command</c> (<c>0x0052aa00</c>, raw
/// 307946-308012), verbatim per
/// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §5c. Covers the
/// command/moving_away pick (plain-towards / plain-away / towards_and_away
/// delegate) crossed with the walk-vs-run HoldKey cascade, INCLUDING the
/// CanCharge 0x10 fast-path ACE dropped (feedback_autowalk_cancharge_bit)
/// and the walk_run_threshhold ≤-vs-&lt; edge (retail: dist - dto ≤
/// threshold → walk; the raw's `test ah,0x41` is the inclusive ≤ reading,
/// §5c @308003).
/// </summary>
public sealed class MovementParametersGetCommandTests
{
// ── plain TOWARDS (move_towards set, move_away clear) ─────────────────
[Fact]
public void PlainTowards_DistGreaterThanDto_WalkForward_NotMovingAway()
{
var p = new MovementParameters { DistanceToObject = 0.6f };
// move_towards=true (default), move_away=false (default)
p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out HoldKey holdKey, out bool movingAway);
Assert.Equal(MotionCommand.WalkForward, motion);
Assert.False(movingAway);
}
[Fact]
public void PlainTowards_DistNotGreaterThanDto_Idle()
{
var p = new MovementParameters { DistanceToObject = 0.6f };
p.GetCommand(dist: 0.6f, headingDiff: 0f, out uint motion, out _, out bool movingAway);
Assert.Equal(0u, motion);
Assert.False(movingAway);
}
[Fact]
public void PlainTowards_DistLessThanDto_Idle()
{
var p = new MovementParameters { DistanceToObject = 0.6f };
p.GetCommand(dist: 0.1f, headingDiff: 0f, out uint motion, out _, out _);
Assert.Equal(0u, motion);
}
// ── pure AWAY (move_away set, move_towards clear) ─────────────────────
[Fact]
public void PureAway_DistLessThanMinDistance_WalkForward_MovingAway()
{
var p = new MovementParameters
{
MoveTowards = false,
MoveAway = true,
MinDistance = 5f,
};
p.GetCommand(dist: 2f, headingDiff: 0f, out uint motion, out _, out bool movingAway);
Assert.Equal(MotionCommand.WalkForward, motion);
Assert.True(movingAway);
}
[Fact]
public void PureAway_DistNotLessThanMinDistance_Idle()
{
var p = new MovementParameters
{
MoveTowards = false,
MoveAway = true,
MinDistance = 5f,
};
p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out _, out bool movingAway);
Assert.Equal(0u, motion);
Assert.False(movingAway);
}
// ── towards_and_away delegate (both move_towards AND move_away set) ───
[Fact]
public void TowardsAndAway_DistGreaterThanDto_DelegatesToWalkForwardTowards()
{
var p = new MovementParameters
{
MoveTowards = true,
MoveAway = true,
DistanceToObject = 0.6f,
MinDistance = 0.2f,
};
p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out _, out bool movingAway);
Assert.Equal(MotionCommand.WalkForward, motion);
Assert.False(movingAway);
}
[Fact]
public void TowardsAndAway_InsideMinBand_WalkBackwards_MovingAway()
{
var p = new MovementParameters
{
MoveTowards = true,
MoveAway = true,
DistanceToObject = 0.6f,
MinDistance = 0.2f,
};
// dist - min_distance < epsilon → inside the min band
p.GetCommand(dist: 0.2f, headingDiff: 0f, out uint motion, out _, out bool movingAway);
Assert.Equal(MotionCommand.WalkBackward, motion);
Assert.True(movingAway);
}
[Fact]
public void TowardsAndAway_InsideDeadband_Idle()
{
var p = new MovementParameters
{
MoveTowards = true,
MoveAway = true,
DistanceToObject = 0.6f,
MinDistance = 0.2f,
};
// strictly inside [min, dto] — neither band fires
p.GetCommand(dist: 0.4f, headingDiff: 0f, out uint motion, out _, out _);
Assert.Equal(0u, motion);
}
// ── neither towards nor away (both clear) — falls to plain-towards path ──
[Fact]
public void NeitherTowardsNorAway_FallsToPlainTowardsBranch()
{
var p = new MovementParameters
{
MoveTowards = false,
MoveAway = false,
DistanceToObject = 0.6f,
};
p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out _, out bool movingAway);
Assert.Equal(MotionCommand.WalkForward, motion);
Assert.False(movingAway);
}
// ── walk-vs-run HoldKey cascade ────────────────────────────────────────
[Fact]
public void HoldKey_CanChargeSet_AlwaysRun_FastPath()
{
// THE fast-path ACE dropped: can_charge (0x10) short-circuits
// straight to HoldKey_Run regardless of distance/threshold.
var p = new MovementParameters
{
CanCharge = true,
CanRun = false, // even with can_run CLEAR
CanWalk = true,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
p.GetCommand(dist: 0.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.Run, holdKey);
}
[Fact]
public void HoldKey_CanRunClear_AlwaysWalk_RegardlessOfDistance()
{
var p = new MovementParameters
{
CanCharge = false,
CanRun = false,
CanWalk = true,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
p.GetCommand(dist: 1000f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.None, holdKey);
}
[Fact]
public void HoldKey_CanRunSet_CanWalkClear_AlwaysRun_WalkIncapable()
{
// can_walk clear → the "close enough to walk" branch is skipped
// entirely; walk-incapable movers always run when can_run is set.
var p = new MovementParameters
{
CanCharge = false,
CanRun = true,
CanWalk = false,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
p.GetCommand(dist: 0.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.Run, holdKey);
}
[Fact]
public void HoldKey_CanRunAndCanWalk_WithinThreshold_Walk()
{
var p = new MovementParameters
{
CanCharge = false,
CanRun = true,
CanWalk = true,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
// dist - dto = 10 <= 15 → walk
p.GetCommand(dist: 10.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.None, holdKey);
}
[Fact]
public void HoldKey_CanRunAndCanWalk_BeyondThreshold_Run()
{
var p = new MovementParameters
{
CanCharge = false,
CanRun = true,
CanWalk = true,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
// dist - dto = 15.1 > 15 → run
p.GetCommand(dist: 15.7f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.Run, holdKey);
}
[Fact]
public void HoldKey_ThresholdEdge_ExactlyAtThreshold_IsInclusive_Walk()
{
// retail: (dist - distance_to_object) <= walk_run_threshhold → WALK.
// The raw's `test ah,0x41` after `fcom` renders as an inclusive
// "not greater than" (≤) — the boundary itself walks, not runs.
var p = new MovementParameters
{
CanCharge = false,
CanRun = true,
CanWalk = true,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
// dist - dto = exactly 15.0
p.GetCommand(dist: 15.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.None, holdKey);
}
[Fact]
public void HoldKey_ThresholdEdge_JustOverThreshold_Run()
{
var p = new MovementParameters
{
CanCharge = false,
CanRun = true,
CanWalk = true,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
// dist - dto = 15.0 + epsilon
p.GetCommand(dist: 15.600001f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.Run, holdKey);
}
[Fact]
public void HoldKey_CanChargeSet_OverridesWalkIncapableAndThreshold()
{
// CanCharge fast-path wins even when every other flag would say walk.
var p = new MovementParameters
{
CanCharge = true,
CanRun = true,
CanWalk = true,
WalkRunThreshhold = 1000f, // would otherwise force walk
DistanceToObject = 0.6f,
};
p.GetCommand(dist: 0.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(HoldKey.Run, holdKey);
}
// ── the four capability quadrants × plain-towards distance bands ──────
[Theory]
// (canRun, canWalk, canCharge, distBeyondThreshold) → expected HoldKey
[InlineData(true, true, false, false, HoldKey.None)] // both capable, close → walk
[InlineData(true, true, false, true, HoldKey.Run)] // both capable, far → run
[InlineData(true, false, false, false, HoldKey.Run)] // run-only, close → still run (no walk branch)
[InlineData(true, false, false, true, HoldKey.Run)] // run-only, far → run
[InlineData(false, true, false, false, HoldKey.None)] // walk-only → always walk
[InlineData(false, true, false, true, HoldKey.None)] // walk-only, far → still walk
[InlineData(false, false, false, false, HoldKey.None)] // neither capable, no charge → walk (falls through)
[InlineData(false, false, true, false, HoldKey.Run)] // can_charge alone → run regardless
public void HoldKey_FourCapabilityQuadrants_MatchRetailCascade(
bool canRun, bool canWalk, bool canCharge, bool distBeyondThreshold, HoldKey expected)
{
var p = new MovementParameters
{
CanRun = canRun,
CanWalk = canWalk,
CanCharge = canCharge,
WalkRunThreshhold = 15f,
DistanceToObject = 0.6f,
};
float dist = distBeyondThreshold ? 20f : 5f; // 20-0.6=19.4>15 ; 5-0.6=4.4<=15
p.GetCommand(dist, headingDiff: 0f, out _, out HoldKey holdKey, out _);
Assert.Equal(expected, holdKey);
}
}