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:
parent
386b1ce550
commit
e0d2492cbb
14 changed files with 1973 additions and 4 deletions
|
|
@ -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-< 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue