fix(R3-W6b): entry-cache the apply pass's axis reads — the 'press W and stop instantly' regression

User live report (the R3 visual pass): pressing W stopped the local
player instantly (retail observers saw run + rubber-band back — our AP
never moved); S/strafe appeared to work. The edge tracer + a
harness-with-echo repro converged on the funnel:

ApplyInterpretedMovement live-read InterpretedState.ForwardCommand
AFTER the style dispatch. The style dispatch SUCCEEDS against the real
MotionTableDispatchSink (GetObjectSequence Branch 1 style==target →
success — verified in the raw @298636), which gates the
ModifyInterpretedState write → InterpretedMotionState::ApplyMotion's
style branch resets forward_command to Ready UNCONDITIONALLY (raw
0051ea6c — our port is verbatim). Retail SELF-HEALS: its compiled
apply pass reads the axis fields into registers BEFORE the style call,
so the fwd dispatch re-applies the pre-reset command and the field
recovers within the pass — proven by our own 183-case live-retail
observer trace (the fwd dispatch carries the wire's RunForward after
the style dispatch on the same UM). The BN pseudo-C's apparent
post-style field reads at 0x528687 are decompiler rendering of hoisted
registers — the same artifact class as the A1 polarity inversion.
Under the live-field read, every apply pass (the ~10Hz ACE UM echo via
ApplyServerRunRate included) dispatched Ready and left the field
permanently Ready; the controller's per-frame get_state_velocity then
zeroed the body. The 183-case suite could not catch it: its
RecordingSink's return value doesn't mirror the real sink's
style-success, so the resetting state-write never ran under test.

Fix: entry-cache fwd/sidestep/turn commands+speeds (and the my_run_rate
read) before the style dispatch — restoring the S2a funnel's original
semantics through the W5 merge.

3 new regression tests bind the REAL sink over a real sequencer
(the masked condition): a full apply pass self-heals forward; 2 seconds
of held-W with echo cadence covers meters not centimeters; Shift
walk↔run toggling survives mid-transition echoes. Full suite: 3,734
passed. Temp diagnostics removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 09:52:23 +02:00
parent 778a2b5385
commit 30115d96aa
2 changed files with 208 additions and 11 deletions

View file

@ -2729,12 +2729,33 @@ public sealed class MotionInterpreter : IMotionDoneSink
_ = cancelMoveTo;
_ = allowJump;
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
MyRunRate = InterpretedState.ForwardSpeed;
// ENTRY-CACHE all axis values BEFORE the style dispatch (W6 stop-bug
// fix, 2026-07-03). The style dispatch's success path runs
// InterpretedMotionState::ApplyMotion(style), whose style branch
// resets forward_command to Ready UNCONDITIONALLY (raw 0051ea6c —
// verbatim). Retail SELF-HEALS because its compiled apply pass reads
// the axis fields into registers before the style call, so the fwd
// dispatch re-applies the pre-reset command (proven by our own
// 183-case live-retail observer trace: the fwd dispatch carries the
// wire's RunForward after the style dispatch on the same UM — the
// BN pseudo-C's apparent post-style field reads at 0x528687 are
// decompiler rendering of hoisted registers, the same artifact
// class as the A1 polarity). A live-field read here dispatched
// READY after every style apply, leaving the field permanently
// Ready — the "press W and stop instantly" W6 regression.
uint entryFwdCmd = InterpretedState.ForwardCommand;
float entryFwdSpeed = InterpretedState.ForwardSpeed;
uint entrySideCmd = InterpretedState.SideStepCommand;
float entrySideSpeed = InterpretedState.SideStepSpeed;
uint entryTurnCmd = InterpretedState.TurnCommand;
float entryTurnSpeed = InterpretedState.TurnSpeed;
if (entryFwdCmd == MotionCommand.RunForward)
MyRunRate = entryFwdSpeed;
DispatchInterpretedMotion(currentStyle, 1.0f, sink);
if (!contact_allows_move(InterpretedState.ForwardCommand))
if (!contact_allows_move(entryFwdCmd))
{
DispatchInterpretedMotion(MotionCommand.Falling, 1.0f, sink);
}
@ -2745,19 +2766,16 @@ public sealed class MotionInterpreter : IMotionDoneSink
}
else
{
DispatchInterpretedMotion(
InterpretedState.ForwardCommand, InterpretedState.ForwardSpeed, sink);
if (InterpretedState.SideStepCommand == 0)
DispatchInterpretedMotion(entryFwdCmd, entryFwdSpeed, sink);
if (entrySideCmd == 0)
DispatchStopInterpretedMotion(MotionCommand.SideStepRight, sink);
else
DispatchInterpretedMotion(
InterpretedState.SideStepCommand, InterpretedState.SideStepSpeed, sink);
DispatchInterpretedMotion(entrySideCmd, entrySideSpeed, sink);
}
if (InterpretedState.TurnCommand != 0)
if (entryTurnCmd != 0)
{
DispatchInterpretedMotion(
InterpretedState.TurnCommand, InterpretedState.TurnSpeed, sink);
DispatchInterpretedMotion(entryTurnCmd, entryTurnSpeed, sink);
return; // retail early return — no idle-stop this call
}