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>
340 lines
11 KiB
C#
340 lines
11 KiB
C#
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);
|
||
}
|
||
}
|