fix(R4): #161 remote landing stuck in falling pose - apply-pass params decode

Retail's apply_interpreted_movement (0x00528600) does NOT dispatch with
ctor-default MovementParameters: the BN decomp smears the bitfield store
into the mush expression at raw 305778 - (word & 0x37ff) | cancelMoveTo<<15
| disableJump<<17 - which CLEARS SetHoldKey/ModifyInterpretedState/
CancelMoveTo. ACE MotionInterp.cs:444-449 confirms independently. Three
legs fixed, all retail decode, no adaptations added:

1. ApplyInterpretedMovement now builds the pass params retail actually
   uses (ModifyInterpretedState=false is load-bearing): no dispatch in the
   pass writes InterpretedState, so the airborne Falling substitution
   PRESERVES the wire's forward command and HitGround's re-apply
   dispatches it - the motion table plays the Falling->X landing link.
   The W6 entry-cache (built on the wrong "retail self-heals via hoisted
   registers" theory) is deleted; live reads are retail semantics. Raw
   arg3 decoded as DisableJumpDuringLink -> every (N,0) caller means
   allowJump=true; all 9 caller polarities fixed. copy_movement_from's
   current_style copy (raw 0051e757) added to the UM flat-copy.

2. Both GameWindow landing blocks cleared the Gravity state bit BEFORE
   Motion.HitGround(), whose verbatim state&0x400 gate then no-opped the
   whole retail re-apply; the UP-driven landing block never called
   HitGround at all. Both now run HitGround (minterp then moveto,
   MovementManager::HitGround 0x00524300 order) with Gravity still set,
   then do the DR bookkeeping clear (register row AP-81 added for the
   remaining flag dance, retire in R6).

3. K-fix17's forced SetCycle (both copies) deleted: it executed every
   landing but read the leg-1-clobbered ForwardCommand (0x40000015) and
   re-set the very Falling cycle it meant to clear.

Tests: new HitGround_AfterFall_RedispatchesPreservedForward_ExitsFalling
lifecycle pin; AirborneBody state assertion flipped (it had pinned the
bug value). Suite 3,964 green incl. the 183-case retail-observer trace.
Filed #164 (action-replay Autonomous bit, no current consumer).

Awaiting live verify: stand-still landing must exit the falling pose with
zero wire input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 17:00:03 +02:00
parent 790938392f
commit b1cf01029a
6 changed files with 280 additions and 199 deletions

View file

@ -13,18 +13,19 @@ namespace AcDream.Core.Tests.Input;
/// <summary>
/// R3-W6 regression suite for the edge-driven local player — specifically
/// the "press W and stop instantly" bug (2026-07-03): the funnel's apply
/// pass live-read <c>InterpretedState.ForwardCommand</c> AFTER the style
/// dispatch, whose <c>ApplyMotion(style)</c> state-write resets forward to
/// Ready unconditionally (raw 0051ea6c, verbatim). Retail self-heals via
/// register-cached entry reads (proven by the 183-case live observer trace:
/// the fwd dispatch carries the pre-reset command); the fix entry-caches
/// the axes in <c>ApplyInterpretedMovement</c>.
/// pass let its own style dispatch's <c>ApplyMotion(style)</c> state-write
/// reset forward to Ready (raw 0051ea6c). The original W6 fix entry-cached
/// the axis fields; #161 replaced that with the TRUE retail mechanism —
/// the apply pass's <c>MovementParameters</c> carry
/// <c>ModifyInterpretedState = false</c> (retail's smeared bitfield store
/// at raw 305778, mask 0x37ff; ACE MotionInterp.cs:447), so NO dispatch in
/// the pass can write <c>InterpretedState</c> at all and live field reads
/// are retail semantics. These tests pin the user-visible invariant either
/// way: pressing W keeps you moving.
///
/// The 183-case suite could NOT catch this: its RecordingSink's ApplyMotion
/// return doesn't mirror the REAL <see cref="MotionTableDispatchSink"/>,
/// which returns TRUE for the style dispatch (manager Branch 1
/// style==target → success) — the true return is what gates the resetting
/// state-write on. These tests bind the REAL sink over a real sequencer.
/// These tests bind the REAL <see cref="MotionTableDispatchSink"/> over a
/// real sequencer (a fake sink's return values can mask state-write gating
/// — the lesson that created this file).
/// </summary>
public class W6EdgeDrivenMovementTests
{
@ -115,7 +116,8 @@ public class W6EdgeDrivenMovementTests
/// trigger was <c>ApplyServerRunRate</c> (the ACE autonomous-echo tap);
/// V5's P1 gate drops that echo before it reaches the player, and the
/// tap is deleted — but the regression these tests pin lives in
/// <c>ApplyInterpretedMovement</c>'s entry-caching, which any
/// <c>ApplyInterpretedMovement</c>'s pass-params state protection
/// (ModifyInterpretedState=false since #161), which any
/// apply_current_movement pass (HitGround re-apply, future R6 per-tick
/// order) still exercises. Same two statements the deleted tap ran.
/// </summary>