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:
parent
790938392f
commit
b1cf01029a
6 changed files with 280 additions and 199 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -177,26 +177,80 @@ public class MotionInterpreterFunnelTests
|
|||
// (0x00528240 early-accept), so it reaches the sink; the style and
|
||||
// forward dispatches are gate-blocked (apply-only path).
|
||||
Assert.Equal(new[] { "DIM 40000015@1.00", "STOP 6500000d" }, sink.Calls);
|
||||
// R3-W5 correction: DoInterpretedMotion's verbatim body (raw
|
||||
// 305575-305631) writes InterpretedState via
|
||||
// InterpretedMotionState::ApplyMotion on BOTH the success path
|
||||
// (raw 305609-305610, gated on the sink's own result) AND the
|
||||
// blocked/apply-only `label_528440` path (raw 305616-305618,
|
||||
// UNCONDITIONAL on the ModifyInterpretedState bit — no sink
|
||||
// involved at all). This W5 slice is the first port to actually
|
||||
// WIRE that state-write (the pre-W5 funnel never called
|
||||
// InterpretedMotionState.ApplyMotion from dispatch at all — state
|
||||
// was set only by MoveToInterpretedState's flat copy at the top of
|
||||
// the call), so the blocked style dispatch DOES flip
|
||||
// ForwardCommand to Ready (0x41000003) at step 1, which then makes
|
||||
// the Falling substitution dispatch (step 2) ITself write
|
||||
// ForwardCommand = Falling (0x40000015) via the SAME mechanism —
|
||||
// this replaces the assertion this comment used to make ("state
|
||||
// still flat-copies") with the verbatim end state. Only
|
||||
// sink.Calls (dispatch ORDER) was ever cdb-verified by this file's
|
||||
// module doc; this resulting-state assertion was an untested
|
||||
// assumption from the pre-W5 architecture.
|
||||
Assert.Equal(0x40000015u, mi.InterpretedState.ForwardCommand);
|
||||
// #161 correction (2026-07-03): the apply pass runs its dispatches
|
||||
// with ModifyInterpretedState = FALSE — retail constructs var_2c
|
||||
// then CLEARS bits 11/14/15 (SetHoldKey / ModifyInterpretedState /
|
||||
// CancelMoveTo) and re-sets 15/17 from the args; the BN pseudo-C
|
||||
// smears that bitfield store into the mush expression at raw
|
||||
// 305778 (`(word & 0x37ff) | (arg2&1)<<15 | (arg3&1)<<17`). ACE
|
||||
// MotionInterp.cs:444-449 confirms independently. So NEITHER the
|
||||
// blocked style dispatch NOR the Falling substitution writes
|
||||
// InterpretedState — the wire's forward command survives the
|
||||
// airborne pass. This is the retail landing-exit mechanism:
|
||||
// HitGround's re-apply dispatches the PRESERVED command, and the
|
||||
// motion table plays the Falling→X landing link. (The previous
|
||||
// revision of this assertion pinned 0x40000015 — the #161 bug
|
||||
// itself: the ctor-default params let the Falling dispatch clobber
|
||||
// forward_command, so a stand-still landing re-dispatched Falling
|
||||
// forever.)
|
||||
Assert.Equal(0x44000007u, mi.InterpretedState.ForwardCommand);
|
||||
Assert.Equal(2.85f, mi.InterpretedState.ForwardSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HitGround_AfterFall_RedispatchesPreservedForward_ExitsFalling()
|
||||
{
|
||||
// #161 (remote jump landing stuck in the falling pose): the full
|
||||
// remote lifecycle. Wire says RunForward@2.85 while grounded; the
|
||||
// body leaves the ground (LeaveGround engages Falling through the
|
||||
// sink WITHOUT clobbering the interpreted forward command — the
|
||||
// apply pass's ModifyInterpretedState=false, raw 305778 / ACE
|
||||
// MotionInterp.cs:447); on touchdown, HitGround (0x00528ac0) —
|
||||
// called with GRAVITY STILL SET, its verbatim state&0x400 gate —
|
||||
// re-applies the PRESERVED command, which is what makes
|
||||
// GetObjectSequence play the Falling→RunForward landing link. No
|
||||
// wire input is needed to exit the falling pose.
|
||||
var body = new PhysicsBody();
|
||||
body.State |= PhysicsStateFlags.Gravity;
|
||||
body.TransientState |= TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable
|
||||
| TransientStateFlags.Active;
|
||||
var mi = new MotionInterpreter(body, new RemoteWeenie());
|
||||
var sink = new RecordingSink();
|
||||
mi.DefaultSink = sink; // HitGround/LeaveGround re-apply through DefaultSink
|
||||
|
||||
mi.MoveToInterpretedState(Ims(fwd: 0x44000007u, fwdSpd: 2.85f), sink);
|
||||
Assert.Equal(0x44000007u, mi.InterpretedState.ForwardCommand);
|
||||
|
||||
// Jump start: ground contact drops FIRST (GameWindow's VectorUpdate
|
||||
// handler order), then LeaveGround re-applies → Falling engages.
|
||||
body.TransientState &= ~(TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable);
|
||||
sink.Calls.Clear();
|
||||
mi.LeaveGround();
|
||||
|
||||
Assert.Contains(sink.Calls, c => c.StartsWith("DIM 40000015"));
|
||||
Assert.Equal(0x44000007u, mi.InterpretedState.ForwardCommand); // NOT clobbered
|
||||
Assert.Equal(2.85f, mi.InterpretedState.ForwardSpeed);
|
||||
|
||||
// Touchdown: contact restored, Gravity still set (the retail
|
||||
// contract — CMotionInterp::HitGround no-ops without it).
|
||||
body.TransientState |= TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
sink.Calls.Clear();
|
||||
mi.HitGround();
|
||||
|
||||
// Retail re-apply order: style (from the copy_movement_from-adopted
|
||||
// interpreted current_style, raw 0051e757) → preserved forward →
|
||||
// sidestep-stop → turn-stop. The forward dispatch at the wire
|
||||
// command IS the falling-pose exit.
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"DIM 8000003d@1.00",
|
||||
"DIM 44000007@2.85",
|
||||
"STOP 6500000f",
|
||||
"STOP 6500000d",
|
||||
}, sink.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue