fix(physics): movement-parity fixes - adjusted catch-up cap, autorun retail semantics, AP-30 retired

Ports CMotionInterp::get_adjusted_max_speed (0x00527D00, byte-decoded:
bare rate unless RunForward; forward_speed x 4.0 when running;
current_speed_factor proven a ctor-constant 1.0 at 0x00528C34) and swaps
all five interpolation catch-up call sites to it - retail's
fUseAdjustedSpeed_ static (.data 0x0081F418 = 1) makes this the live
branch, so standing/walking remotes now catch up at ~2x runRate instead
of 4x too fast (the #41/#165 presentation family). Autorun now hard-
forces Run for its duration and cancels on every fresh forward press
(CommandInterpreter::HandleNewForwardMovement 0x006b3d60 is literally
SetAutoRun(0,1)); the old test pin codified the divergence. AP-30
retired: retail Frame::is_equal genuinely uses the 0.0002 epsilon - the
row recorded a non-divergence. Three catch-up test pins re-baselined to
retail semantics with citations. Full Release suite 9,983/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-30 15:04:17 +02:00
parent 2123b44e8c
commit c0afcacbb2
10 changed files with 174 additions and 32 deletions

View file

@ -666,6 +666,69 @@ public sealed class MotionInterpreterTests
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.75f, speed, precision: 4);
}
// =========================================================================
// GetAdjustedMaxSpeed (CMotionInterp::get_adjusted_max_speed @ 0x00527D00)
// Movement parity audit 2026-07-30 — the accessor InterpolationManager's
// catch-up cap actually uses (fUseAdjustedSpeed_ = 0x1, .data 0x0081F418).
// Byte-decoded: NOT RunForward → bare rate; RunForward → forward_speed ×
// RunAnimSpeed (current_speed_factor is a ctor-constant 1.0, 0x00528C34).
// =========================================================================
[Theory]
[InlineData(MotionCommand.WalkForward)]
[InlineData(MotionCommand.WalkBackward)]
[InlineData(MotionCommand.Ready)]
public void GetAdjustedMaxSpeed_NotRunForward_ReturnsBareRunRate(uint command)
{
// The 4x-too-fast walking-remote catch-up fix: a non-RunForward mover's
// adjusted max speed is the BARE rate — no RunAnimSpeed multiply
// (0x00527d2a jnz falls straight to ret with rate in st0).
var weenie = new FakeWeenie { RunRate = 2.94f };
var interp = MakeInterp(weenie: weenie);
interp.InterpretedState.ForwardCommand = command;
Assert.Equal(2.94f, interp.GetAdjustedMaxSpeed(), precision: 4);
}
[Fact]
public void GetAdjustedMaxSpeed_RunForward_ReturnsForwardSpeedTimesRunAnimSpeed()
{
// RunForward DISCARDS the queried rate (fstp st0) and returns
// forward_speed / current_speed_factor(=1.0) * 4.0.
var weenie = new FakeWeenie { RunRate = 99f }; // must be ignored
var interp = MakeInterp(weenie: weenie);
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
interp.InterpretedState.ForwardSpeed = 2.5f;
Assert.Equal(
2.5f * MotionInterpreter.RunAnimSpeed,
interp.GetAdjustedMaxSpeed(),
precision: 4); // 10.0 — NOT 99×4
}
[Fact]
public void GetAdjustedMaxSpeed_NoWeenie_FallsBackToLiteralOne()
{
// 0x00527d0b: no weenie → fld [0x007928b0] = 1.0 (NOT my_run_rate).
var interp = MakeInterp(weenie: null);
interp.MyRunRate = 3.5f;
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
Assert.Equal(1.0f, interp.GetAdjustedMaxSpeed(), precision: 4);
}
[Fact]
public void GetAdjustedMaxSpeed_InqRunRateFails_FallsBackToMyRunRate()
{
// 0x00527d1d-0x00527d23: InqRunRate false → fld [this+0x7c] my_run_rate.
var weenie = new FakeWeenie { RunRate = 5f, InqRunRateResult = false };
var interp = MakeInterp(weenie: weenie);
interp.MyRunRate = 2.4f;
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
Assert.Equal(2.4f, interp.GetAdjustedMaxSpeed(), precision: 4);
}
[Fact]
public void GetMaxSpeed_NoWeenie_ReturnsLiteralOneTimesRunAnimSpeed()
{