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

@ -5403,27 +5403,30 @@ public sealed class GameWindow : IDisposable
{
rmState.Airborne = false;
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
rmState.Interp.Clear();
rmState.Body.Position = worldPos;
// Reset the sequencer out of Falling — see matching block in
// TickAnimations Step 5 (env-var path) for rationale.
// #161: retail landing = MovementManager::HitGround
// (minterp → moveto, 0x00524300) with the Gravity state
// bit STILL SET (CMotionInterp::HitGround gates on
// state&0x400). The re-apply dispatches the PRESERVED
// pre-fall forward command → landing link → cycle. This
// replaces the forced SetCycle, which read the
// then-clobbered ForwardCommand (Falling) and re-set the
// pose it meant to clear. See the twin block in
// TickAnimations (VU.land).
if (_animatedEntities.TryGetValue(entity.Id, out var aeForLand)
&& aeForLand.Sequencer is not null)
{
uint style = aeForLand.Sequencer.CurrentStyle != 0
? aeForLand.Sequencer.CurrentStyle
: 0x8000003Du;
uint landingCmd = rmState.Motion.InterpretedState.ForwardCommand;
if (landingCmd == 0)
landingCmd = AcDream.Core.Physics.MotionCommand.Ready;
float landingSpeed = rmState.Motion.InterpretedState.ForwardSpeed;
if (landingSpeed <= 0f) landingSpeed = 1f;
aeForLand.Sequencer.SetCycle(style, landingCmd, landingSpeed);
EnsureRemoteMotionBindings(rmState, aeForLand, update.Guid);
}
rmState.Motion.HitGround();
rmState.MoveTo?.HitGround();
// DR bookkeeping only (partner of the jump-start
// `State |= Gravity`).
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
return;
}
@ -10045,9 +10048,22 @@ public sealed class GameWindow : IDisposable
rm.Airborne = false;
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
rm.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
rm.Body.Velocity = new System.Numerics.Vector3(
rm.Body.Velocity.X, rm.Body.Velocity.Y, 0f);
// #161: HitGround MUST run with the Gravity state
// bit still set — CMotionInterp::HitGround
// (0x00528ac0) gates on state&0x400 (retail never
// clears GRAVITY on landing; it's a persistent
// object property). Clearing it first made this
// re-apply a silent no-op, which is why the
// falling pose never exited. The re-apply
// dispatches the PRESERVED pre-fall forward
// command through the funnel → the motion table
// plays the Falling→X landing link. (The old
// K-fix17 forced SetCycle is deleted: it read the
// then-clobbered InterpretedState.ForwardCommand
// — 0x40000015 — and re-set the very Falling
// cycle it meant to clear.)
rm.Motion.HitGround();
// R4-V5 (closes the V4 wiring-contract gap the
// adversarial review caught): retail order —
@ -10057,34 +10073,10 @@ public sealed class GameWindow : IDisposable
// without it a chasing NPC that lands stalls
// until ACE's ~1 Hz re-emit.
rm.MoveTo?.HitGround();
// K-fix17 (2026-04-26): reset the sequencer cycle
// from Falling back to whatever the interpreted
// motion state says they should be doing now.
// Without this, the remote stays in the Falling
// pose forever (legs folded) until the next
// server-sent UpdateMotion arrives. Use the
// sequencer's current style (preserved across
// jump) and pick the cycle from
// InterpretedState.ForwardCommand: Ready
// (idle), WalkForward, RunForward, WalkBackward.
// SideStep / Turn aren't strict locomotion
// priorities — the next UM the server sends will
// refine the cycle if the player is mid-strafe
// when they land; this just gets the legs out
// of Falling immediately.
if (ae.Sequencer is not null)
{
uint style = ae.Sequencer.CurrentStyle != 0
? ae.Sequencer.CurrentStyle
: 0x8000003Du;
uint landingCmd = rm.Motion.InterpretedState.ForwardCommand;
if (landingCmd == 0)
landingCmd = AcDream.Core.Physics.MotionCommand.Ready;
float landingSpeed = rm.Motion.InterpretedState.ForwardSpeed;
if (landingSpeed <= 0f) landingSpeed = 1f;
ae.Sequencer.SetCycle(style, landingCmd, landingSpeed);
}
// DR bookkeeping only (partner of the jump-start
// `State |= Gravity`): stops the per-tick gravity
// integration for the grounded body.
rm.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
Console.WriteLine($"VU.land guid=0x{serverGuid:X8} Z={rm.Body.Position.Z:F2}");