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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 16:18:40 +02:00
parent ab35a78c1d
commit 41006e795a
3 changed files with 115 additions and 1 deletions

View file

@ -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(),
};
}
}

View file

@ -0,0 +1,54 @@
namespace AcDream.Core.Physics;
/// <summary>
/// R4-V5 (#160 fix, 2026-07-03): the minimal weenie every REMOTE entity's
/// <see cref="MotionInterpreter"/> carries — standing in for retail's
/// per-object <c>ACCWeenieObject</c>, which every placed physics object has.
///
/// <para>
/// The load-bearing behavior is the RUN-RATE FALLBACK CHAIN:
/// <c>apply_run_to_command</c> (0x00527be0, raw 305062-305076) reads
/// <c>weenie ? (InqRunRate() ?: my_run_rate) : 1.0</c>. Remote weenies in
/// retail FAIL <c>InqRunRate</c> (no local skill data), landing on
/// <c>my_run_rate</c> — exactly the field the mt-6/7 unpack writes with the
/// wire's <c>MoveToRunRate</c> (M13). Our remote interps previously had NO
/// weenie at all, taking the degenerate <c>else 1.0</c> 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).
/// </para>
///
/// <para>
/// <see cref="IWeenieObject.IsThePlayer"/> 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.
/// <see cref="IWeenieObject.IsCreature"/> 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).
/// </para>
/// </summary>
public sealed class RemoteWeenie : IWeenieObject
{
/// <summary>Remote objects have no local skill data — fail, so the
/// retail fallback chain lands on <see cref="MotionInterpreter.MyRunRate"/>
/// (the wire-fed rate).</summary>
public bool InqRunRate(out float rate)
{
rate = 0f;
return false;
}
/// <summary>Remotes never locally compute jump arcs (server-broadcast
/// VectorUpdate velocities drive them).</summary>
public bool InqJumpVelocity(float extent, out float vz)
{
vz = 0f;
return false;
}
/// <summary>Remotes never locally initiate jumps.</summary>
public bool CanJump(float extent) => true;
}

View file

@ -0,0 +1,52 @@
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// R4-V5 #160 fix: retail's apply_run_to_command (0x00527be0) run-rate
/// chain is <c>weenie ? (InqRunRate() ?: my_run_rate) : 1.0</c>. 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).
/// </summary>
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);
}
}