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:
parent
2123b44e8c
commit
c0afcacbb2
10 changed files with 174 additions and 32 deletions
|
|
@ -87,14 +87,22 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardDoesNotCancelAutorunAndResetDoes()
|
||||
public void ForwardCancelsAutorun_AndResetClearsIt()
|
||||
{
|
||||
// Movement parity audit 2026-07-30 — retail
|
||||
// CommandInterpreter::HandleNewForwardMovement (0x006b3d60) is
|
||||
// literally `SetAutoRun(0, 1)`: EVERY fresh forward press disables
|
||||
// autorun and hands control back to the key. The previous pin
|
||||
// ("ForwardDoesNotCancelAutorun") codified the divergence.
|
||||
var source = CreateSource();
|
||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||
|
||||
Assert.False(source.HandlePressedAction(InputAction.MovementForward));
|
||||
Assert.True(source.AutoRunActive);
|
||||
|
||||
source.HandlePressedAction(InputAction.MovementForward);
|
||||
Assert.False(source.AutoRunActive);
|
||||
|
||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||
Assert.True(source.AutoRunActive);
|
||||
source.ResetSession();
|
||||
Assert.False(source.AutoRunActive);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,9 +300,14 @@ public sealed class RemotePhysicsUpdaterTests
|
|||
ownerClockEpoch: binding.Record.ObjectClockEpoch);
|
||||
|
||||
// InterpolationManager::adjust_offset clamps the 1 m correction to
|
||||
// maxSpeed (8 m/s) * 0.1 s = 0.8 m and replaces the authored 10 m
|
||||
// root origin rather than adding to it.
|
||||
Assert.InRange(motion.Body.Position.X, 10.79f, 10.81f);
|
||||
// 2 x GetAdjustedMaxSpeed x 0.1 s. Movement parity audit 2026-07-30:
|
||||
// retail's cap uses get_adjusted_max_speed (fUseAdjustedSpeed_ = 1,
|
||||
// .data 0x0081F418) — for this fixture's Ready interpreted state that
|
||||
// is the BARE rate (1.0), so 2 m/s x 0.1 s = 0.2 m, and the clamped
|
||||
// correction replaces the authored 10 m root origin rather than
|
||||
// adding to it. (The former 10.8 pin encoded the old unconditional
|
||||
// GetMaxSpeed cap — 4x retail for a non-running remote.)
|
||||
Assert.InRange(motion.Body.Position.X, 10.19f, 10.21f);
|
||||
Assert.InRange(
|
||||
MathF.Abs(Quaternion.Dot(
|
||||
Quaternion.Normalize(target),
|
||||
|
|
|
|||
|
|
@ -216,12 +216,22 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
||||
float startX = entity.Position.X;
|
||||
|
||||
scheduler.Tick(
|
||||
0.1f,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1);
|
||||
// Movement parity audit 2026-07-30: retail's catch-up cap is
|
||||
// 2 x GetAdjustedMaxSpeed — the BARE rate (1.0) for this fixture's
|
||||
// Ready interpreted state, i.e. 0.2 m per 0.1 s tick (the old
|
||||
// single-tick > 0.5 m expectation encoded the unconditional
|
||||
// GetMaxSpeed cap, 4x retail for a non-running remote). The intent
|
||||
// pinned here is CONSUMPTION: drive enough ticks for the 1 m
|
||||
// correction to be fully absorbed.
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
scheduler.Tick(
|
||||
0.1f,
|
||||
entity.Position,
|
||||
localHiddenPartPoseDirty: false,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1);
|
||||
}
|
||||
|
||||
Assert.True(entity.Position.X > startX + 0.5f);
|
||||
Assert.True(remote.Interp.IsActive || entity.Position.X >= startX + 0.99f);
|
||||
|
|
@ -354,7 +364,10 @@ public sealed class LiveEntityAnimationSchedulerTests
|
|||
Assert.Contains(record.ProjectionKey!.Value, schedules.Keys);
|
||||
Assert.Equal(1, rootPublishes);
|
||||
float distance = animation.Entity.Position.X - startX;
|
||||
Assert.InRange(distance, 0.79f, 0.81f);
|
||||
// Movement parity audit 2026-07-30: 2 x GetAdjustedMaxSpeed (bare
|
||||
// rate 1.0 for a Ready mover) x 0.1 s = 0.2 m per tick — the former
|
||||
// 0.8 pin encoded the old unconditional GetMaxSpeed cap.
|
||||
Assert.InRange(distance, 0.19f, 0.21f);
|
||||
Assert.Same(remote.Body, record.PhysicsBody);
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue