From 9c5634af17de959098028e6c2ec3b0181e416957 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 2 May 2026 19:16:38 +0200 Subject: [PATCH] feat(physics): MotionInterpreter.GetMaxSpeed for InterpolationManager (L.3.1 Task 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports retail's CMotionInterp::get_max_speed (0x00527cb0). Returns motion-table-derived max speed (m/s) for InterpretedState.ForwardCommand: - RunForward: RunAnimSpeed (4.0) × (InqRunRate ?? MyRunRate) - WalkForward: WalkAnimSpeed (3.12) - WalkBackward: WalkAnimSpeed × 0.65 (BackwardsFactor from adjust_motion @ 0x00528010) - otherwise: 0 Decomp note: Binary Ninja emits a spurious void return for x87 FPU-returning functions; the actual float return is confirmed by both callers (StickyManager::adjust_offset @ 0x00555430, InterpolationManager::AdjustOffset @ 0x00555d52) which multiply the result by 2.0 to produce a catch-up speed in m/s. The per-command switch is consistent with get_state_velocity (0x00527d50) which uses the same constants. Used by InterpolationManager.AdjustOffset in Task 5 as 2 × GetMaxSpeed(). Until Task 5 wires it, the method is unused — covered by 4 unit tests. Co-Authored-By: Claude Opus 4.7 --- src/AcDream.Core/Physics/MotionInterpreter.cs | 50 +++++++++++++++++ .../Physics/MotionInterpreterTests.cs | 56 +++++++++++++++++++ 2 files changed, 106 insertions(+) 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); + } }