diff --git a/src/AcDream.Core/Physics/MotionInterpreter.cs b/src/AcDream.Core/Physics/MotionInterpreter.cs
index 038f675..1930b44 100644
--- a/src/AcDream.Core/Physics/MotionInterpreter.cs
+++ b/src/AcDream.Core/Physics/MotionInterpreter.cs
@@ -932,6 +932,56 @@ public sealed class MotionInterpreter
apply_current_movement(cancelMoveTo: false, allowJump: true);
}
+ // ── CMotionInterp::get_max_speed (0x00527cb0) ─────────────────────────────
+
+ ///
+ /// Return the motion-table-derived max speed (m/s) for the current
+ /// .
+ ///
+ ///
+ /// Retail reference (named-retail, 0x00527cb0):
+ /// CMotionInterp::get_max_speed fetches the run rate via
+ /// InqRunRate (or falls back to my_run_rate) and returns
+ /// the result as a float from the x87 FPU stack (ST0). The Binary Ninja
+ /// decompiler emits a spurious void return type for x87-returning
+ /// functions — the actual return value is confirmed by the two callers:
+ /// StickyManager::adjust_offset (0x00555430) and
+ /// InterpolationManager::AdjustOffset (0x00555d52), both of which
+ /// multiply the result by 2.0 to produce a catch-up speed in m/s. With a
+ /// run rate of ~1.0 the catch-up would be 2.0 m/s — far too slow — so the
+ /// function must return the actual velocity (m/s), not a bare rate.
+ ///
+ ///
+ ///
+ /// The per-command switch mirrors get_state_velocity
+ /// (0x00527d50), which uses the same constants and the same
+ /// RunForward / WalkForward / WalkBackward branches, and the
+ /// adjust_motion (0x00527c0e) BackwardsFactor = 0.65
+ /// scaling confirmed at address 0x00528010.
+ ///
+ ///
+ ///
+ /// Used by InterpolationManager.AdjustOffset in L.3 Task 5
+ /// as 2 × GetMaxSpeed() catch-up speed.
+ ///
+ ///
+ public float GetMaxSpeed()
+ {
+ // Resolve current run rate: prefer WeenieObj.InqRunRate, fall back to MyRunRate.
+ // Mirrors the InqRunRate query at the top of CMotionInterp::get_max_speed.
+ float rate = MyRunRate;
+ if (WeenieObj is not null && WeenieObj.InqRunRate(out float queried))
+ rate = queried;
+
+ return InterpretedState.ForwardCommand switch
+ {
+ MotionCommand.RunForward => RunAnimSpeed * rate,
+ MotionCommand.WalkForward => WalkAnimSpeed,
+ MotionCommand.WalkBackward => WalkAnimSpeed * 0.65f, // BackwardsFactor @ adjust_motion 0x00528010
+ _ => 0f, // idle / non-locomotion
+ };
+ }
+
// ── private helper ────────────────────────────────────────────────────────
///
diff --git a/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs b/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
index 1892611..addc9fa 100644
--- a/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
@@ -817,4 +817,60 @@ public sealed class MotionInterpreterTests
var vel = mi.get_state_velocity();
Assert.Equal(4.0f * 2.375f, vel.Y, precision: 2);
}
+
+ // =========================================================================
+ // GetMaxSpeed (CMotionInterp::get_max_speed @ 0x00527cb0)
+ // L.3.1 Task 2 — InterpolationManager catch-up speed source
+ // =========================================================================
+
+ [Fact]
+ public void GetMaxSpeed_RunForward_ReturnsRunAnimSpeedTimesRunRate()
+ {
+ // Retail: get_max_speed returns run rate from InqRunRate; callers
+ // multiply by 2 to get catch-up speed. For RunForward the per-m/s
+ // speed is RunAnimSpeed × rate = 4.0 × 1.5 = 6.0.
+ var weenie = new FakeWeenie { RunRate = 1.5f };
+ var interp = MakeInterp(weenie: weenie);
+ interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
+
+ float speed = interp.GetMaxSpeed();
+
+ Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.5f, speed, precision: 4); // 6.0
+ }
+
+ [Fact]
+ public void GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed()
+ {
+ // WalkForward max speed is always WalkAnimSpeed (3.12) — no run-rate scaling.
+ var interp = MakeInterp();
+ interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
+
+ float speed = interp.GetMaxSpeed();
+
+ Assert.Equal(MotionInterpreter.WalkAnimSpeed, speed, precision: 4);
+ }
+
+ [Fact]
+ public void GetMaxSpeed_WalkBackward_ReturnsWalkAnimSpeedTimesBackwardsFactor()
+ {
+ // BackwardsFactor = 0.65, from adjust_motion @ 0x00528010 in the named retail decomp.
+ var interp = MakeInterp();
+ interp.InterpretedState.ForwardCommand = MotionCommand.WalkBackward;
+
+ float speed = interp.GetMaxSpeed();
+
+ Assert.Equal(MotionInterpreter.WalkAnimSpeed * 0.65f, speed, precision: 4);
+ }
+
+ [Fact]
+ public void GetMaxSpeed_Idle_ReturnsZero()
+ {
+ // Ready / non-locomotion commands → 0 (no movement speed).
+ var interp = MakeInterp();
+ interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
+
+ float speed = interp.GetMaxSpeed();
+
+ Assert.Equal(0f, speed, precision: 4);
+ }
}