From 41006e795a06096e23076fb485eaf465888111c8 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 3 Jul 2026 16:18:40 +0200 Subject: [PATCH] fix(R4-V5): #160 remote run movetos in slow motion - remote interps had no weenie, so retail's run-rate chain never reached my_run_rate User observation (acdream observing a retail player's server MoveTo): walk-distance approaches correct; run-distance plays the run cycle AND moves toward the target in slow motion. Root cause, raw-verified (apply_run_to_command 0x00527be0, raw 305062-305076): retail's rate chain is weenie ? (InqRunRate() ?: my_run_rate) : 1.0 Every placed retail object HAS a weenie; a REMOTE's weenie fails InqRunRate (no local skill data) and the chain lands on my_run_rate - the exact field retail's mt-6/7 unpack writes with the wire's MoveToRunRate (M13; observed 4.50). Our port had the branch verbatim but remote interps carried NO weenie at all, taking the else-1.0 branch retail reserves for weenie-less detached objects: observer-side run dispatches went out at speed 1.0, so the run cycle's pace AND get_state_velocity's chase speed both crawled. Walk was unaffected - walk speed is never rate-scaled (the discriminating symptom). Fix: RemoteWeenie (Core) - the minimal stand-in for retail's per-object ACCWeenieObject: InqRunRate fails (-> my_run_rate), InqJumpVelocity fails, IsThePlayer default false (also ends the fragile "null weenie counts as the player" reading of the A3 dual dispatch for remotes), IsCreature default true (doors carry it too; their force-asserted Contact keeps the creature ground-gate moot - documented on the class). RemoteMotion's interp now constructs with it. Tests: RemoteWeenieRunRateTests pins both branches against the raw - RemoteWeenie + MyRunRate=4.5 promotes WalkForward->RunForward @ 4.5; weenie-less keeps the verbatim degenerate 1.0. Full suite green: 3,963. Co-Authored-By: Claude Fable 5 --- src/AcDream.App/Rendering/GameWindow.cs | 10 +++- src/AcDream.Core/Physics/RemoteWeenie.cs | 54 +++++++++++++++++++ .../Physics/RemoteWeenieRunRateTests.cs | 52 ++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/AcDream.Core/Physics/RemoteWeenie.cs create mode 100644 tests/AcDream.Core.Tests/Physics/RemoteWeenieRunRateTests.cs diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 015c5c3b..39acb9d9 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -610,7 +610,15 @@ public sealed class GameWindow : IDisposable // transition link (see PhysicsBody.InWorld). InWorld = true, }; - Motion = new AcDream.Core.Physics.MotionInterpreter(Body); + Motion = new AcDream.Core.Physics.MotionInterpreter(Body) + { + // R4-V5 #160 fix: retail remotes carry a weenie whose + // InqRunRate FAILS, landing apply_run_to_command on + // my_run_rate (the M13 wire feed). A null weenie took the + // degenerate 1.0 branch — observer-side run movetos played + // and moved in slow motion. See RemoteWeenie. + WeenieObj = new AcDream.Core.Physics.RemoteWeenie(), + }; } } diff --git a/src/AcDream.Core/Physics/RemoteWeenie.cs b/src/AcDream.Core/Physics/RemoteWeenie.cs new file mode 100644 index 00000000..6d6aedb9 --- /dev/null +++ b/src/AcDream.Core/Physics/RemoteWeenie.cs @@ -0,0 +1,54 @@ +namespace AcDream.Core.Physics; + +/// +/// R4-V5 (#160 fix, 2026-07-03): the minimal weenie every REMOTE entity's +/// carries — standing in for retail's +/// per-object ACCWeenieObject, which every placed physics object has. +/// +/// +/// The load-bearing behavior is the RUN-RATE FALLBACK CHAIN: +/// apply_run_to_command (0x00527be0, raw 305062-305076) reads +/// weenie ? (InqRunRate() ?: my_run_rate) : 1.0. Remote weenies in +/// retail FAIL InqRunRate (no local skill data), landing on +/// my_run_rate — exactly the field the mt-6/7 unpack writes with the +/// wire's MoveToRunRate (M13). Our remote interps previously had NO +/// weenie at all, taking the degenerate else 1.0 branch retail +/// reserves for detached objects — so an observer-side moveto dispatched +/// RunForward at speed 1.0 instead of the wire rate: the run cycle played +/// in slow motion and the chase velocity crawled (walk was unaffected — +/// walk speed isn't rate-scaled). +/// +/// +/// +/// stays default false — the A3 +/// dual dispatch routes remotes through apply_interpreted_movement, ending +/// the fragile "null weenie counts as the player" reading of that gate. +/// stays default true; static +/// animated objects (doors) also carry this weenie — retail would say +/// non-creature there, but their RemoteMotion bodies force-assert Contact, +/// so the creature ground-gate is satisfied either way (noted, not a +/// behavior difference today). +/// +/// +public sealed class RemoteWeenie : IWeenieObject +{ + /// Remote objects have no local skill data — fail, so the + /// retail fallback chain lands on + /// (the wire-fed rate). + public bool InqRunRate(out float rate) + { + rate = 0f; + return false; + } + + /// Remotes never locally compute jump arcs (server-broadcast + /// VectorUpdate velocities drive them). + public bool InqJumpVelocity(float extent, out float vz) + { + vz = 0f; + return false; + } + + /// Remotes never locally initiate jumps. + public bool CanJump(float extent) => true; +} diff --git a/tests/AcDream.Core.Tests/Physics/RemoteWeenieRunRateTests.cs b/tests/AcDream.Core.Tests/Physics/RemoteWeenieRunRateTests.cs new file mode 100644 index 00000000..e54d8aa3 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/RemoteWeenieRunRateTests.cs @@ -0,0 +1,52 @@ +using AcDream.Core.Physics; +using Xunit; + +namespace AcDream.Core.Tests.Physics; + +/// +/// R4-V5 #160 fix: retail's apply_run_to_command (0x00527be0) run-rate +/// chain is weenie ? (InqRunRate() ?: my_run_rate) : 1.0. Remote +/// weenies FAIL InqRunRate, landing on my_run_rate — the field the mt-6/7 +/// unpack writes with the wire's MoveToRunRate (M13). These pin that a +/// RemoteWeenie-equipped interp scales the run promotion by MyRunRate +/// (observer-side movetos ran in slow motion when the interp had no +/// weenie and took the degenerate 1.0 branch). +/// +public class RemoteWeenieRunRateTests +{ + [Fact] + public void ApplyRunToCommand_RemoteWeenie_ScalesByWireRunRate() + { + var interp = new MotionInterpreter(new PhysicsBody()) + { + WeenieObj = new RemoteWeenie(), + MyRunRate = 4.5f, // the mt-6 wire write (unpack @300603) + }; + + uint motion = MotionCommand.WalkForward; + float speed = 1.0f; + interp.apply_run_to_command(ref motion, ref speed); + + Assert.Equal(MotionCommand.RunForward, motion); + Assert.Equal(4.5f, speed, 3); + } + + [Fact] + public void ApplyRunToCommand_NoWeenie_KeepsRetailDegenerateBranch() + { + // Verbatim retail: a weenie-LESS (detached-class) object scales by + // 1.0 (raw 305062-305076 `else x87_r7 = 1f`). Production bodies all + // carry a weenie (player: PlayerWeenie; remotes: RemoteWeenie). + var interp = new MotionInterpreter(new PhysicsBody()) + { + MyRunRate = 4.5f, + }; + + uint motion = MotionCommand.WalkForward; + float speed = 1.0f; + interp.apply_run_to_command(ref motion, ref speed); + + Assert.Equal(MotionCommand.RunForward, motion); + Assert.Equal(1.0f, speed, 3); + } +}