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
|
|
@ -68,7 +68,13 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
|||
StrafeRight: dispatcher.IsActionHeld(InputAction.MovementStrafeRight),
|
||||
TurnLeft: dispatcher.IsActionHeld(InputAction.MovementTurnLeft),
|
||||
TurnRight: dispatcher.IsActionHeld(InputAction.MovementTurnRight),
|
||||
Run: !walking,
|
||||
// Movement parity audit (2026-07-30): retail's autorun hard-forces
|
||||
// Run for its whole duration (ACCmdInterp autorun dispatch always
|
||||
// sends RunForward-class state); the live walk/run toggle only
|
||||
// applies to ordinary held-key movement. Recomputing `!walking`
|
||||
// unconditionally let a walk-mode toggle demote an active autorun
|
||||
// to walking — impossible in retail.
|
||||
Run: !walking || AutoRunActive,
|
||||
Jump: dispatcher.IsActionHeld(InputAction.MovementJump));
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +90,12 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
|||
AcDream.Runtime.RuntimeMovementCommand.ToggleRunLock);
|
||||
|
||||
if (AutoRunActive && action is (
|
||||
InputAction.MovementBackup
|
||||
// Movement parity audit (2026-07-30): retail's
|
||||
// ACCmdInterp::HandleNewForwardMovement drops autorun on EVERY
|
||||
// fresh forward press edge, not only on backward/stop/strafe —
|
||||
// a new W press while autorunning hands control back to the key.
|
||||
InputAction.MovementForward
|
||||
or InputAction.MovementBackup
|
||||
or InputAction.MovementStop
|
||||
or InputAction.MovementStrafeLeft
|
||||
or InputAction.MovementStrafeRight))
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ internal sealed class PlayerModeController :
|
|||
playerGuid,
|
||||
playerEntity).Radius,
|
||||
inContact: () => capturedController.BodyInContact,
|
||||
minterpMaxSpeed: () => capturedController.Motion.GetMaxSpeed(),
|
||||
minterpMaxSpeed: () => capturedController.Motion.GetAdjustedMaxSpeed(),
|
||||
curTime: () => capturedController.SimTimeSeconds,
|
||||
physicsTimerTime: () => capturedController.SimTimeSeconds,
|
||||
getObjectA: _motionBindings.ResolvePhysicsHost,
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
getVelocity: () => mtBody.Velocity,
|
||||
getRadius: () => GetSetupCylinder(serverGuid, selfEntity).Radius,
|
||||
inContact: () => mtBody.OnWalkable,
|
||||
minterpMaxSpeed: () => rmT.Motion.GetMaxSpeed(),
|
||||
minterpMaxSpeed: () => rmT.Motion.GetAdjustedMaxSpeed(),
|
||||
curTime: NowSeconds,
|
||||
physicsTimerTime: NowSeconds,
|
||||
getObjectA: ResolvePhysicsHost,
|
||||
|
|
|
|||
|
|
@ -2617,16 +2617,13 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Consequence: the dead-reckoning catch-up speed
|
||||
/// (<c>InterpolationManager::adjust_offset</c> 0x00555d30, pc:353122)
|
||||
/// is <c>2 × get_max_speed()</c> ≈ 23.5 m/s for a run-rate-2.94
|
||||
/// (run-skill-200) character — that IS retail's value. An earlier
|
||||
/// doc-comment here claimed the bare rate (~5.9 m/s catch-up) was
|
||||
/// retail-correct and blamed the ×4 for the multi-second 1-Hz blip on
|
||||
/// observed retail remotes; that reading trusted the BN x87 dropout
|
||||
/// and is refuted by the binary. If the blip recurs, its root cause is
|
||||
/// elsewhere (node-fail handling / progress-quantum abandonment /
|
||||
/// position-queue feed — the #41 family), NOT this multiply.
|
||||
/// Consequence note SUPERSEDED 2026-07-30 (movement parity audit): the
|
||||
/// dead-reckoning catch-up cap is <c>2 ×
|
||||
/// <see cref="GetAdjustedMaxSpeed"/></c>, not 2 × this — the
|
||||
/// <c>fUseAdjustedSpeed_</c> static defaults the 0x00555dbe branch to
|
||||
/// the ADJUSTED accessor (bare rate unless actively RunForward). This
|
||||
/// method remains retail-faithful for its other consumers; only the
|
||||
/// catch-up-cap sites moved to the adjusted sibling below.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public float GetMaxSpeed()
|
||||
|
|
@ -2641,6 +2638,52 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
|||
return RunAnimSpeed * rate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CMotionInterp::get_adjusted_max_speed</c> (0x00527D00) — the
|
||||
/// accessor <c>InterpolationManager</c>'s catch-up cap ACTUALLY uses,
|
||||
/// because the static <c>InterpolationManager::fUseAdjustedSpeed_</c>
|
||||
/// initializes to <c>0x1</c> (.data 0x0081F418, pseudo-C:1102675), making
|
||||
/// the <c>get_max_speed</c> branch at 0x00555dbe dead by default.
|
||||
///
|
||||
/// <para>
|
||||
/// Byte decode of 0x00527D00 (movement parity audit + P-review,
|
||||
/// 2026-07-30): compute rate = InqRunRate() / <see cref="MyRunRate"/> /
|
||||
/// 1.0 fallback chain, then <c>cmp [this+0x4C], 0x44000007</c>
|
||||
/// (RunForward) — NOT RunForward returns the BARE rate (no ×4);
|
||||
/// RunForward DISCARDS the rate (<c>fstp st0</c>) and returns
|
||||
/// <c>interpreted_state.forward_speed ÷ current_speed_factor ×
|
||||
/// RunAnimSpeed(4.0)</c>. <c>current_speed_factor</c> (+0x6C) is written
|
||||
/// exactly once in the entire binary — <c>CMotionInterp::Create</c>
|
||||
/// (0x00528C34) stores <c>0x3F800000</c> = 1.0f — so the division is a
|
||||
/// vestigial constant and is not ported as a field.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Consequence: retail caps remote catch-up at <c>2 ×</c> THIS — a
|
||||
/// standing/walking/turning remote catches up at ~2×runRate (≈5.9 m/s at
|
||||
/// run-rate 2.94), not 2×<see cref="GetMaxSpeed"/> (≈23.5 m/s). The
|
||||
/// former unconditional <see cref="GetMaxSpeed"/> at the catch-up sites
|
||||
/// made non-running remotes reconverge ~4× faster than retail (the
|
||||
/// #41/#165 presentation family).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public float GetAdjustedMaxSpeed()
|
||||
{
|
||||
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||||
{
|
||||
// 0x00527d31-0x00527d40: fstp st0 (discard rate); fld forward_speed;
|
||||
// fdiv current_speed_factor (ctor-constant 1.0); fmul 4.0.
|
||||
return InterpretedState.ForwardSpeed * RunAnimSpeed;
|
||||
}
|
||||
|
||||
// Not RunForward: the bare rate — NO RunAnimSpeed multiply
|
||||
// (0x00527d2a jnz falls straight to ret with rate in st0).
|
||||
float rate = 1.0f;
|
||||
if (WeenieObj is not null && !WeenieObj.InqRunRate(out rate))
|
||||
rate = MyRunRate;
|
||||
return rate;
|
||||
}
|
||||
|
||||
// R3-W5: the former `ApplyMotionToInterpretedState` private helper
|
||||
// (a hand-rolled switch approximating InterpretedMotionState.ApplyMotion)
|
||||
// is DELETED per the plan (closes J3/J4) — the merged DoInterpretedMotion
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
rm.PositionManagerDeltaScratch;
|
||||
pmDelta.Origin = scaledRootMotionLocalOrigin;
|
||||
pmDelta.Orientation = rootMotionLocalFrame.Orientation;
|
||||
float maxSpeedNpc = rm.Motion.GetMaxSpeed();
|
||||
float maxSpeedNpc = rm.Motion.GetAdjustedMaxSpeed();
|
||||
System.Numerics.Vector3? terrainNormalNpc = !rm.Airborne
|
||||
? _physics.Engine.SampleTerrainNormal(
|
||||
rm.Body.Position.X,
|
||||
|
|
@ -288,7 +288,7 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
rm.PositionManagerDeltaScratch;
|
||||
pmDelta.Origin = scaledRootMotionLocalOrigin;
|
||||
pmDelta.Orientation = rootMotionLocalFrame.Orientation;
|
||||
float maxSpeedNpc = rm.Motion.GetMaxSpeed();
|
||||
float maxSpeedNpc = rm.Motion.GetAdjustedMaxSpeed();
|
||||
System.Numerics.Vector3? terrainNormalNpc = !rm.Airborne
|
||||
? _physics.Engine.SampleTerrainNormal(
|
||||
rm.Body.Position.X,
|
||||
|
|
@ -682,7 +682,7 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
rm.Body.Orientation,
|
||||
positionDelta,
|
||||
rm.Interp,
|
||||
rm.Motion.GetMaxSpeed(),
|
||||
rm.Motion.GetAdjustedMaxSpeed(),
|
||||
positionDelta,
|
||||
inContact: rm.Body.InContact);
|
||||
rm.Host?.PositionManager.AdjustOffset(positionDelta, dt);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue