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
|
|
@ -73,31 +73,60 @@ actually steps at (Player_Move.cs / Creature GetRunRate usage).
|
||||||
|
|
||||||
## #161 — Remote jump landing holds the falling pose (no landing anim)
|
## #161 — Remote jump landing holds the falling pose (no landing anim)
|
||||||
|
|
||||||
**Status:** OPEN (2026-07-03; retested after TS-40/`41006e79` — still
|
**Status:** FIX LANDED (2026-07-03) — awaiting user live verify.
|
||||||
broken; user: stuck in falling until the char moves or the window loses
|
**Root cause (three legs, all retail-decode):**
|
||||||
focus).
|
1. **Apply-pass params (the big one):** retail's `apply_interpreted_movement`
|
||||||
**Evidence (launch.log, session bklydthrr, guid 0x50000001):** `VU.land`
|
runs ALL its dispatches with `ModifyInterpretedState=false` — the ctor
|
||||||
fires; Falling completions (0x40000015) flow; but the FIRST post-landing
|
default 0x1EE0F is REWRITTEN by a bitfield store the BN decomp smears
|
||||||
UM still reports `seq now motion=0x40000015` — the landing block's reset
|
into the mush expression at raw 305778 (`(word & 0x37ff) |
|
||||||
(rm.Motion.HitGround() apply + K-fix17 SetCycle) did NOT move the cycle
|
cancelMoveTo<<15 | disableJump<<17`; 0x37ff clears SetHoldKey/
|
||||||
off Falling. Recovery happened only via SUBSEQUENT wire UMs (the mover
|
ModifyInterpretedState/CancelMoveTo). ACE MotionInterp.cs:444-449
|
||||||
pressed keys); a stand-still landing gets none → stuck forever. The
|
confirms. Our pass dispatched with ctor defaults (modify=true), so the
|
||||||
focus-change unstick hints a frame-advance/big-dt interaction as well.
|
airborne Falling substitution CLOBBERED `InterpretedState.ForwardCommand`
|
||||||
**Prime suspect:** the funnel's airborne branch dispatches Falling via
|
→ HitGround's re-apply re-dispatched Falling instead of the preserved
|
||||||
DoInterpretedMotion with ctor-default ModifyInterpretedState=true →
|
pre-fall wire command. (The handoff's "HitGround raw @305949" was a
|
||||||
InterpretedState.ForwardCommand becomes 0x40000015; HitGround's
|
mislabel — 305949 is inside `move_to_interpreted_state`; the real
|
||||||
apply_current_movement (interpreted branch for remotes) then re-applies
|
HitGround is 0x00528ac0 → `apply_current_movement(0,0)`.) Bonus decode:
|
||||||
the interpreted state — i.e. re-dispatches FALLING — instead of the
|
raw arg3 = DisableJumpDuringLink, so every `(N, 0)` caller means
|
||||||
pre-fall wire command. Check retail: does apply_interpreted_movement's
|
allowJump=TRUE — all caller polarities fixed. The W6 entry-cache (whose
|
||||||
falling dispatch (raw ~305730, fresh local var_2c params) really write
|
"retail self-heals via hoisted registers" theory was this same
|
||||||
interpreted fwd (bit 0x4000 in the local's 0x1EE0F default) — and if so,
|
misreading) is deleted; live field reads are retail semantics now that
|
||||||
what exits Falling on a retail REMOTE landing (HitGround raw @305949
|
the pass cannot write state.
|
||||||
apply_current_movement(this, 1, ...) — the arg2=1 semantics)? Also
|
2. **Landing-order:** both GameWindow landing blocks cleared the Gravity
|
||||||
verify K-fix17's SetCycle actually executes there (add a probe) and
|
state bit BEFORE `rm.Motion.HitGround()`, whose verbatim `state & 0x400`
|
||||||
whether it lost a fight with the same-frame HitGround apply ordering.
|
gate then no-opped the entire retail re-apply (retail never clears
|
||||||
**Where:** GameWindow remote landing block (`VU.land` region);
|
GRAVITY on landing). The UP-driven landing block never called HitGround
|
||||||
MotionInterpreter.ApplyInterpretedMovement falling branch;
|
at all. Both now: transients → `Motion.HitGround()` → `MoveTo.HitGround()`
|
||||||
HitGround/apply_current_movement.
|
→ THEN the DR bookkeeping gravity-clear.
|
||||||
|
3. **K-fix17 answered (user Q3):** its SetCycle DID execute — and re-set
|
||||||
|
the Falling cycle, because it read the leg-1-clobbered ForwardCommand
|
||||||
|
(0x40000015 ≠ 0, so the Ready fallback never fired). Both SetCycle
|
||||||
|
adaptation blocks deleted — superseded by the retail HitGround path,
|
||||||
|
which also plays the proper Falling→X landing LINK animation SetCycle
|
||||||
|
never did. Also fixed en route: `copy_movement_from` omitted
|
||||||
|
`current_style` (raw 0051e757 copies it first) — the landing re-apply's
|
||||||
|
style dispatch read a stale style.
|
||||||
|
**Tests:** `MotionInterpreterFunnelTests.HitGround_AfterFall_
|
||||||
|
RedispatchesPreservedForward_ExitsFalling` (full lifecycle) + the flipped
|
||||||
|
`AirborneBody_NoCycleDispatches_OnlyTurnStop` state assertion (it had
|
||||||
|
PINNED the bug value 0x40000015 — noted in-file). Suite 3,964 green.
|
||||||
|
**Verify:** retail mover jumps in place near acdream observer → lands →
|
||||||
|
legs exit falling pose into idle WITHOUT pressing any key; jump while
|
||||||
|
running → lands into run cycle.
|
||||||
|
|
||||||
|
## #164 — UM action-replay dispatches drop the per-action Autonomous bit
|
||||||
|
|
||||||
|
**Status:** OPEN (2026-07-03, filed during #161)
|
||||||
|
**Finding:** retail's `move_to_interpreted_state` action loop sets the
|
||||||
|
dispatch params' Autonomous bit (0x1000) from each action's autonomy flag
|
||||||
|
(raw 305982: `var_28 ^= ((autonomous << 0xc) ^ var_28) & 0x1000`), which
|
||||||
|
flows into `InterpretedMotionState::AddAction`'s stored node. Our
|
||||||
|
`MoveToInterpretedState` action loop dispatches with `new
|
||||||
|
MovementParameters { Speed }` — Autonomous stays false. No observed
|
||||||
|
symptom today (stored-node autonomy has no current consumer); fix when
|
||||||
|
R5/R6 touches the action list. **Where:**
|
||||||
|
`MotionInterpreter.MoveToInterpretedState` action loop /
|
||||||
|
`DispatchInterpretedMotion` (doc comment marks the gap).
|
||||||
|
|
||||||
## #162 — Observer-side moveto cancelled by ACE's autonomous MTS reflections (the "glide" class)
|
## #162 — Observer-side moveto cancelled by ACE's autonomous MTS reflections (the "glide" class)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Documented approximation (AP) — 72 rows
|
## 3. Documented approximation (AP) — 73 rows
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
|
@ -179,6 +179,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| AP-77 | **`apply_current_movement`'s interpreted-branch tail (`ApplyCurrentMovementInterpreted`) writes body velocity DIRECTLY via `get_state_velocity`/`set_local_velocity` when grounded, instead of dispatching through an `IInterpretedMotionSink`** — retail's real `apply_interpreted_movement` (0x00528600) drives velocity indirectly through `DoInterpretedMotion`'s animation-table backend (the SAME function the funnel's `ApplyInterpretedMovement` already uses WITH a sink); this dual-dispatch call site (`HitGround`/`LeaveGround`/`ReportExhaustion`/hold-key toggles/`SetWeenieObject`/`SetPhysicsObject`) has no sink threaded through it | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`) | Direct continuation of the pre-R3-W4 `apply_current_movement` approximation (R3-W4 plan explicitly keeps this shape rather than relocating it — "the direct grounded-velocity write MOVES to the controller-side call site unchanged"); correct for the grounded, no-animation-table-yet state acdream is in today | A MotionTable whose locomotion cycle bakes a DIFFERENT velocity than `get_state_velocity`'s constants would silently diverge from what the animation actually plays, since this path never touches the animation backend at all | `CMotionInterp::apply_interpreted_movement` 0x00528600; retire when a sink is threaded through `apply_current_movement`'s callers (R6 root motion) |
|
| AP-77 | **`apply_current_movement`'s interpreted-branch tail (`ApplyCurrentMovementInterpreted`) writes body velocity DIRECTLY via `get_state_velocity`/`set_local_velocity` when grounded, instead of dispatching through an `IInterpretedMotionSink`** — retail's real `apply_interpreted_movement` (0x00528600) drives velocity indirectly through `DoInterpretedMotion`'s animation-table backend (the SAME function the funnel's `ApplyInterpretedMovement` already uses WITH a sink); this dual-dispatch call site (`HitGround`/`LeaveGround`/`ReportExhaustion`/hold-key toggles/`SetWeenieObject`/`SetPhysicsObject`) has no sink threaded through it | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`) | Direct continuation of the pre-R3-W4 `apply_current_movement` approximation (R3-W4 plan explicitly keeps this shape rather than relocating it — "the direct grounded-velocity write MOVES to the controller-side call site unchanged"); correct for the grounded, no-animation-table-yet state acdream is in today | A MotionTable whose locomotion cycle bakes a DIFFERENT velocity than `get_state_velocity`'s constants would silently diverge from what the animation actually plays, since this path never touches the animation backend at all | `CMotionInterp::apply_interpreted_movement` 0x00528600; retire when a sink is threaded through `apply_current_movement`'s callers (R6 root motion) |
|
||||||
| AP-79 | **P4 TargetTracker minimal adapter**: MoveToManager's setTarget/clearTarget seams store the tracked guid on RemoteMotion (remotes, R4-V4) or the `_playerMoveToTarget*` GameWindow fields (local player, R4-V5); GameWindow's per-tick block (remotes) / pre-Update feed (player) delivers `HandleUpdateTarget(Ok)` from the live entity table whenever the target moved beyond the set_target radius (and `ExitWorld` when the entity despawns) — retail's TargetManager is a full subscription system with per-target quantums and callback scheduling (R4-V4 + R4-V5, 2026-07-03) | `src/AcDream.App/Rendering/GameWindow.cs` (the tracker block in TickAnimations + the setTarget seam in EnsureRemoteMotionBindings; the player twin: `_playerMoveToTarget*` fields, the EnterPlayerModeNow setTarget seam, and the pre-Update feed in OnUpdateFrame) | The manager's deferred-start/retarget lifecycle only needs position deltas at the tracked radius; the adapter delivers exactly that from data the client already holds | Update cadence is frame-quantized rather than quantum-scheduled; a target moving sub-radius never re-fires (retail same); multi-listener semantics absent until R5's TargetManager port | `TargetManager::ReceiveUpdate` chain (r4-moveto-decomp.md); retire in R5 |
|
| AP-79 | **P4 TargetTracker minimal adapter**: MoveToManager's setTarget/clearTarget seams store the tracked guid on RemoteMotion (remotes, R4-V4) or the `_playerMoveToTarget*` GameWindow fields (local player, R4-V5); GameWindow's per-tick block (remotes) / pre-Update feed (player) delivers `HandleUpdateTarget(Ok)` from the live entity table whenever the target moved beyond the set_target radius (and `ExitWorld` when the entity despawns) — retail's TargetManager is a full subscription system with per-target quantums and callback scheduling (R4-V4 + R4-V5, 2026-07-03) | `src/AcDream.App/Rendering/GameWindow.cs` (the tracker block in TickAnimations + the setTarget seam in EnsureRemoteMotionBindings; the player twin: `_playerMoveToTarget*` fields, the EnterPlayerModeNow setTarget seam, and the pre-Update feed in OnUpdateFrame) | The manager's deferred-start/retarget lifecycle only needs position deltas at the tracked radius; the adapter delivers exactly that from data the client already holds | Update cadence is frame-quantized rather than quantum-scheduled; a target moving sub-radius never re-fires (retail same); multi-listener semantics absent until R5's TargetManager port | `TargetManager::ReceiveUpdate` chain (r4-moveto-decomp.md); retire in R5 |
|
||||||
| AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables (R4-V4 note; pre-existing mechanism, row added per the V4 plan) | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) |
|
| AP-80 | **PlanFromVelocity survives for velocity-only NPC cycles** (M16): UpdatePosition-derived speed picks Ready/Walk/Run cycles for server-controlled creatures whose UMs never arrive (scripted-path NPCs); retail derives every cycle from motion messages through the motion tables (R4-V4 note; pre-existing mechanism, row added per the V4 plan) | `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (`PlanFromVelocity`); consumer `GameWindow.ApplyServerControlledVelocityCycle` | Some ACE entities move by position updates alone — without this, they slide in T-pose; constants (StopSpeed 0.2, RunThreshold 1.25) tuned against live ACE traffic | Cycle-pick thresholds are acdream inventions — a creature intended to walk fast may show run legs near the threshold | retire in R6 (root motion + full per-tick order) |
|
||||||
|
| AP-81 | **Remote-DR gravity toggled via the Gravity STATE bit**: the jump handler sets `Body.State \|= Gravity` at VectorUpdate and both landing blocks clear it after `HitGround()`; retail keeps GRAVITY set for the object's whole life and gates gravity ACCELERATION on the Contact transient (`calc_acceleration`) (pre-existing K-fix9/K-fix15 mechanism, row added during #161 — which also fixed the ordering so `Motion.HitGround()`'s verbatim `state&0x400` gate runs BEFORE the clear) | `src/AcDream.App/Rendering/GameWindow.cs` (VectorUpdate jump handler + the two landing blocks) | The DR tick integrates gravity only for airborne remotes; the flag dance delivers exactly that without porting the full contact-gated `calc_acceleration` chain; the #161 ordering fix keeps the retail HitGround contract satisfied | Any NEW call into `Motion.HitGround`/`LeaveGround` placed after the clear silently no-ops on the gravity gate (the #161 leg-2 class); grounded remotes carry a non-retail state word (probes comparing state bits vs retail mislead) | `CPhysicsObj::calc_acceleration` (contact-gated); `set_on_walkable` 0x00511310; retire in R6 (contact-gated accel + persistent GRAVITY) |
|
||||||
|
|
||||||
## 4. Temporary stopgap (TS) — 36 rows (TS-37 is a retired-row historical note, not an active count)
|
## 4. Temporary stopgap (TS) — 36 rows (TS-37 is a retired-row historical note, not an active count)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5403,27 +5403,30 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
rmState.Airborne = false;
|
rmState.Airborne = false;
|
||||||
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||||
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
|
||||||
rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||||
rmState.Interp.Clear();
|
rmState.Interp.Clear();
|
||||||
rmState.Body.Position = worldPos;
|
rmState.Body.Position = worldPos;
|
||||||
|
|
||||||
// Reset the sequencer out of Falling — see matching block in
|
// #161: retail landing = MovementManager::HitGround
|
||||||
// TickAnimations Step 5 (env-var path) for rationale.
|
// (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)
|
if (_animatedEntities.TryGetValue(entity.Id, out var aeForLand)
|
||||||
&& aeForLand.Sequencer is not null)
|
&& aeForLand.Sequencer is not null)
|
||||||
{
|
{
|
||||||
uint style = aeForLand.Sequencer.CurrentStyle != 0
|
EnsureRemoteMotionBindings(rmState, aeForLand, update.Guid);
|
||||||
? 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);
|
|
||||||
}
|
}
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10045,9 +10048,22 @@ public sealed class GameWindow : IDisposable
|
||||||
rm.Airborne = false;
|
rm.Airborne = false;
|
||||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||||
rm.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
|
||||||
rm.Body.Velocity = new System.Numerics.Vector3(
|
rm.Body.Velocity = new System.Numerics.Vector3(
|
||||||
rm.Body.Velocity.X, rm.Body.Velocity.Y, 0f);
|
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();
|
rm.Motion.HitGround();
|
||||||
// R4-V5 (closes the V4 wiring-contract gap the
|
// R4-V5 (closes the V4 wiring-contract gap the
|
||||||
// adversarial review caught): retail order —
|
// adversarial review caught): retail order —
|
||||||
|
|
@ -10057,34 +10073,10 @@ public sealed class GameWindow : IDisposable
|
||||||
// without it a chasing NPC that lands stalls
|
// without it a chasing NPC that lands stalls
|
||||||
// until ACE's ~1 Hz re-emit.
|
// until ACE's ~1 Hz re-emit.
|
||||||
rm.MoveTo?.HitGround();
|
rm.MoveTo?.HitGround();
|
||||||
|
// DR bookkeeping only (partner of the jump-start
|
||||||
// K-fix17 (2026-04-26): reset the sequencer cycle
|
// `State |= Gravity`): stops the per-tick gravity
|
||||||
// from Falling back to whatever the interpreted
|
// integration for the grounded body.
|
||||||
// motion state says they should be doing now.
|
rm.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||||
Console.WriteLine($"VU.land guid=0x{serverGuid:X8} Z={rm.Body.Position.Z:F2}");
|
Console.WriteLine($"VU.land guid=0x{serverGuid:X8} Z={rm.Body.Position.Z:F2}");
|
||||||
|
|
|
||||||
|
|
@ -1749,11 +1749,14 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
||||||
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
||||||
{
|
{
|
||||||
apply_raw_movement(cancelMoveTo: false, allowJump: false);
|
// Raw (0, 0): arg3 is DisableJumpDuringLink → allowJump: true
|
||||||
|
// (#161 polarity decode; consumed since the apply-pass params
|
||||||
|
// became real).
|
||||||
|
apply_raw_movement(cancelMoveTo: false, allowJump: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyCurrentMovementInterpreted(cancelMoveTo: false, allowJump: false);
|
ApplyCurrentMovementInterpreted(cancelMoveTo: false, allowJump: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FUN_00528920 / FUN_00528970 — SetWeenieObject / SetPhysicsObject ───────
|
// ── FUN_00528920 / FUN_00528970 — SetWeenieObject / SetPhysicsObject ───────
|
||||||
|
|
@ -1800,11 +1803,13 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
bool isThePlayer = weenie is null || weenie.IsThePlayer();
|
bool isThePlayer = weenie is null || weenie.IsThePlayer();
|
||||||
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
||||||
{
|
{
|
||||||
apply_raw_movement(cancelMoveTo: true, allowJump: false);
|
// Raw (1, 0): cancelMoveTo set, DisableJumpDuringLink clear →
|
||||||
|
// allowJump: true (#161 polarity decode).
|
||||||
|
apply_raw_movement(cancelMoveTo: true, allowJump: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyCurrentMovementInterpreted(cancelMoveTo: true, allowJump: false);
|
ApplyCurrentMovementInterpreted(cancelMoveTo: true, allowJump: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1848,11 +1853,12 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
||||||
if (isThePlayer && physicsObj.LastMoveWasAutonomous)
|
if (isThePlayer && physicsObj.LastMoveWasAutonomous)
|
||||||
{
|
{
|
||||||
apply_raw_movement(cancelMoveTo: true, allowJump: false);
|
// Raw (1, 0) → allowJump: true (#161 polarity decode).
|
||||||
|
apply_raw_movement(cancelMoveTo: true, allowJump: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyCurrentMovementInterpreted(cancelMoveTo: true, allowJump: false);
|
ApplyCurrentMovementInterpreted(cancelMoveTo: true, allowJump: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FUN_00527a50 — jump_charge_is_allowed ─────────────────────────────────
|
// ── FUN_00527a50 — jump_charge_is_allowed ─────────────────────────────────
|
||||||
|
|
@ -2506,7 +2512,8 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
JumpExtent = 0f;
|
JumpExtent = 0f;
|
||||||
|
|
||||||
RemoveLinkAnimations?.Invoke();
|
RemoveLinkAnimations?.Invoke();
|
||||||
apply_current_movement(cancelMoveTo: false, allowJump: false);
|
// Raw (0, 0) → allowJump: true (#161 polarity decode).
|
||||||
|
apply_current_movement(cancelMoveTo: false, allowJump: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FUN_00528ac0 — HitGround ──────────────────────────────────────────────
|
// ── FUN_00528ac0 — HitGround ──────────────────────────────────────────────
|
||||||
|
|
@ -2551,7 +2558,11 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
return;
|
return;
|
||||||
|
|
||||||
RemoveLinkAnimations?.Invoke();
|
RemoveLinkAnimations?.Invoke();
|
||||||
apply_current_movement(cancelMoveTo: false, allowJump: false);
|
// Raw (0, 0) → allowJump: true (#161 polarity decode). The
|
||||||
|
// re-apply dispatches the PRESERVED interpreted forward command
|
||||||
|
// (the apply pass never writes it — ModifyInterpretedState=false)
|
||||||
|
// — this IS the falling-pose exit on landing.
|
||||||
|
apply_current_movement(cancelMoveTo: false, allowJump: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FUN_00528c80 — enter_default_state ────────────────────────────────────
|
// ── FUN_00528c80 — enter_default_state ────────────────────────────────────
|
||||||
|
|
@ -2638,7 +2649,8 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
if (runKeyUp != notCurrentlyRun)
|
if (runKeyUp != notCurrentlyRun)
|
||||||
{
|
{
|
||||||
RawState.CurrentHoldKey = holdingRun ? HoldKey.Run : HoldKey.None;
|
RawState.CurrentHoldKey = holdingRun ? HoldKey.Run : HoldKey.None;
|
||||||
apply_current_movement(cancelMoveTo: interrupt, allowJump: false);
|
// Raw (arg3, 0) → allowJump: true (#161 polarity decode).
|
||||||
|
apply_current_movement(cancelMoveTo: interrupt, allowJump: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2694,13 +2706,14 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
if (current == HoldKey.Run)
|
if (current == HoldKey.Run)
|
||||||
{
|
{
|
||||||
RawState.CurrentHoldKey = HoldKey.None;
|
RawState.CurrentHoldKey = HoldKey.None;
|
||||||
apply_current_movement(cancelMoveTo, allowJump: false);
|
// Raw (arg3, 0) → allowJump: true (#161 polarity decode).
|
||||||
|
apply_current_movement(cancelMoveTo, allowJump: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (key == HoldKey.Run && current != HoldKey.Run)
|
else if (key == HoldKey.Run && current != HoldKey.Run)
|
||||||
{
|
{
|
||||||
RawState.CurrentHoldKey = HoldKey.Run;
|
RawState.CurrentHoldKey = HoldKey.Run;
|
||||||
apply_current_movement(cancelMoveTo, allowJump: false);
|
apply_current_movement(cancelMoveTo, allowJump: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2815,6 +2828,11 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
bool allowJump = MotionAllowsJump(InterpretedState.ForwardCommand) == WeenieError.None;
|
bool allowJump = MotionAllowsJump(InterpretedState.ForwardCommand) == WeenieError.None;
|
||||||
|
|
||||||
// copy_movement_from — flat overwrite, no per-field presence checks.
|
// copy_movement_from — flat overwrite, no per-field presence checks.
|
||||||
|
// current_style is the FIRST copied field (raw 0051e757) — it is
|
||||||
|
// what HitGround/LeaveGround re-applies as the style dispatch
|
||||||
|
// (#161: previously omitted, so the landing re-apply read a stale
|
||||||
|
// interpreted style).
|
||||||
|
InterpretedState.CurrentStyle = ims.CurrentStyle;
|
||||||
InterpretedState.ForwardCommand = ims.ForwardCommand;
|
InterpretedState.ForwardCommand = ims.ForwardCommand;
|
||||||
InterpretedState.ForwardSpeed = ims.ForwardSpeed;
|
InterpretedState.ForwardSpeed = ims.ForwardSpeed;
|
||||||
InterpretedState.SideStepCommand = ims.SideStepCommand;
|
InterpretedState.SideStepCommand = ims.SideStepCommand;
|
||||||
|
|
@ -2872,24 +2890,31 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>R3-W4 (closes J11's param-plumbing leg):</b> <paramref name="cancelMoveTo"/>/
|
/// <b>#161 (2026-07-03, closes R3-W4's TODO(W5)):</b> retail builds ONE
|
||||||
/// <paramref name="allowJump"/> are retail's <c>arg2</c>/<c>arg3</c> —
|
/// <c>MovementParameters</c> for the whole pass
|
||||||
/// threaded through NOW per the plan (every real caller already has an
|
/// (<c>MovementParameters::MovementParameters(&var_2c)</c> @305719)
|
||||||
/// opinion: <see cref="apply_current_movement"/>'s dual-dispatch tail
|
/// and then rewrites its bitfield: the "garbled tail expression" at raw
|
||||||
/// passes its own args through unchanged; <see cref="MoveToInterpretedState"/>
|
/// 305778 is the BN decompiler smearing that store into its single read
|
||||||
/// passes <c>(true, motion_allows_jump(OLD forward_command) == 0)</c> per
|
/// site — <c>(word & 0x37ff) | (arg2&1)<<15 |
|
||||||
/// the Adjacent finding). Their ONLY retail consumer in this function is
|
/// (arg3&1)<<17</c> clears bits 11/14/15 (<c>SetHoldKey</c> /
|
||||||
/// the garbled tail expression at raw 305778
|
/// <c>ModifyInterpretedState</c> / <c>CancelMoveTo</c>), re-sets bit 15
|
||||||
/// (<c>(((arg3&1)<<2)|(arg2&1))<<0xf</c> feeding an
|
/// from <c>arg2</c> (= <paramref name="cancelMoveTo"/>) and bit 17 from
|
||||||
/// uninitialized-local byte test before conditionally calling
|
/// <c>arg3</c> (= <c>DisableJumpDuringLink</c>, so
|
||||||
/// <c>InterpretedMotionState::RemoveMotion(0x6500000d)</c> a SECOND time)
|
/// <paramref name="allowJump"/> = <c>arg3 == 0</c>). ACE
|
||||||
/// — a BN x87/uninit-local artifact class, not a readable retail
|
/// MotionInterp.cs:444-449 confirms independently. The load-bearing bit
|
||||||
/// algorithm. TODO(W5): resolve that tail against a clean decompile (or
|
/// is <c>ModifyInterpretedState = false</c>: NO dispatch issued by this
|
||||||
/// cdb capture) once <c>MovementParameters</c> flows through this
|
/// pass writes <see cref="InterpretedState"/> — the airborne Falling
|
||||||
/// funnel end to end; until then the params are accepted and preserved
|
/// substitution leaves the wire's forward command intact, which is what
|
||||||
/// for signature parity but do not change this function's control flow
|
/// lets <see cref="HitGround"/>'s re-apply dispatch the PRESERVED
|
||||||
/// (matches the funnel's existing "no MovementParameters yet" posture,
|
/// pre-fall command (the motion table then plays the Falling→X landing
|
||||||
/// e.g. <see cref="DispatchInterpretedMotion"/>'s own TODO(W5)).
|
/// link — the falling-pose exit needs no wire input). The previous
|
||||||
|
/// revision dispatched with ctor-default params
|
||||||
|
/// (<c>ModifyInterpretedState = true</c>) — the #161 stuck-falling-pose
|
||||||
|
/// root cause, and the true mechanism behind the W6 "press W and stop
|
||||||
|
/// instantly" regression (the style dispatch could never have clobbered
|
||||||
|
/// forward_command in retail; the entry-cache workaround this body used
|
||||||
|
/// to carry is deleted — with no mid-pass state writes possible, live
|
||||||
|
/// field reads ARE retail's semantics).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ApplyInterpretedMovement(
|
public void ApplyInterpretedMovement(
|
||||||
|
|
@ -2898,67 +2923,61 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
{
|
{
|
||||||
if (PhysicsObj is null) return;
|
if (PhysicsObj is null) return;
|
||||||
|
|
||||||
// cancelMoveTo/allowJump: accepted for signature parity (see the
|
// Retail's rewritten var_2c (raw 305778 decoded; ACE 444-449):
|
||||||
// TODO(W5) above) — not yet consumed by this function's body.
|
// Speed is re-set per axis below, everything else rides the pass.
|
||||||
_ = cancelMoveTo;
|
var p = new MovementParameters
|
||||||
_ = allowJump;
|
|
||||||
|
|
||||||
// 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(entryFwdCmd))
|
|
||||||
{
|
{
|
||||||
DispatchInterpretedMotion(MotionCommand.Falling, 1.0f, sink);
|
SetHoldKey = false,
|
||||||
|
ModifyInterpretedState = false,
|
||||||
|
CancelMoveTo = cancelMoveTo,
|
||||||
|
DisableJumpDuringLink = !allowJump,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||||||
|
MyRunRate = InterpretedState.ForwardSpeed;
|
||||||
|
|
||||||
|
p.Speed = 1.0f;
|
||||||
|
DoInterpretedMotion(currentStyle, p, sink);
|
||||||
|
|
||||||
|
if (!contact_allows_move(InterpretedState.ForwardCommand))
|
||||||
|
{
|
||||||
|
p.Speed = 1.0f; // raw 305728: var_18_2 = 0x3f800000
|
||||||
|
DoInterpretedMotion(MotionCommand.Falling, p, sink);
|
||||||
}
|
}
|
||||||
else if (StandingLongJump)
|
else if (StandingLongJump)
|
||||||
{
|
{
|
||||||
DispatchInterpretedMotion(MotionCommand.Ready, 1.0f, sink);
|
p.Speed = 1.0f;
|
||||||
DispatchStopInterpretedMotion(MotionCommand.SideStepRight, sink);
|
DoInterpretedMotion(MotionCommand.Ready, p, sink);
|
||||||
|
StopInterpretedMotion(MotionCommand.SideStepRight, p, sink);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DispatchInterpretedMotion(entryFwdCmd, entryFwdSpeed, sink);
|
p.Speed = InterpretedState.ForwardSpeed;
|
||||||
if (entrySideCmd == 0)
|
DoInterpretedMotion(InterpretedState.ForwardCommand, p, sink);
|
||||||
DispatchStopInterpretedMotion(MotionCommand.SideStepRight, sink);
|
if (InterpretedState.SideStepCommand == 0)
|
||||||
|
{
|
||||||
|
StopInterpretedMotion(MotionCommand.SideStepRight, p, sink);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
DispatchInterpretedMotion(entrySideCmd, entrySideSpeed, sink);
|
{
|
||||||
|
p.Speed = InterpretedState.SideStepSpeed;
|
||||||
|
DoInterpretedMotion(InterpretedState.SideStepCommand, p, sink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entryTurnCmd != 0)
|
if (InterpretedState.TurnCommand != 0)
|
||||||
{
|
{
|
||||||
DispatchInterpretedMotion(entryTurnCmd, entryTurnSpeed, sink);
|
p.Speed = InterpretedState.TurnSpeed;
|
||||||
|
DoInterpretedMotion(InterpretedState.TurnCommand, p, sink);
|
||||||
return; // retail early return — no idle-stop this call
|
return; // retail early return — no idle-stop this call
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tail (raw 305766-305786): unconditional StopInterpretedMotion(TurnRight,
|
// Tail (raw 305766-305786): unconditional StopInterpretedMotion(TurnRight,
|
||||||
// params) — the merged StopInterpretedMotion ITSELF performs the
|
// params) — the merged StopInterpretedMotion ITSELF performs the
|
||||||
// add_to_queue(context, Ready, 0) + RemoveMotion(TurnRight) on
|
// add_to_queue(context, Ready, 0) on success; with the pass params'
|
||||||
// success, so no separate AddToQueue call is needed here anymore
|
// ModifyInterpretedState=false the RemoveMotion(TurnRight) state
|
||||||
// (R3-W5 moves this bookkeeping into the merged function body).
|
// clear does NOT fire (retail identical — ACE 497-498).
|
||||||
DispatchStopInterpretedMotion(MotionCommand.TurnRight, sink);
|
StopInterpretedMotion(MotionCommand.TurnRight, p, sink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -3263,33 +3282,17 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// R3-W5 internal per-axis dispatch primitive
|
/// R3-W5 internal dispatch primitive (formerly the S2a funnel's public
|
||||||
/// (<c>DispatchInterpretedMotion</c>, formerly the S2a funnel's public
|
/// surface), now used ONLY by <see cref="MoveToInterpretedState"/>'s
|
||||||
/// surface). <see cref="ApplyInterpretedMovement"/>'s four
|
/// action-replay loop — retail's action dispatches (raw 305983) use the
|
||||||
/// <c>DoInterpretedMotion</c>-shaped calls (style, forward-or-Falling,
|
/// UM apply's ctor-default <c>var_2c</c> (notably
|
||||||
/// sidestep, turn) each need to dispatch through an EXPLICIT per-call
|
/// <c>ModifyInterpretedState = true</c>, so replayed actions DO enter the
|
||||||
/// <paramref name="sink"/> (a remote entity's own sink, threaded in from
|
/// interpreted actions list via <c>InterpretedMotionState::AddAction</c>),
|
||||||
/// <see cref="MoveToInterpretedState"/>) — retail's own
|
/// unlike the apply pass's rewritten params (see
|
||||||
/// <c>apply_interpreted_movement</c> calls <c>CMotionInterp::DoInterpretedMotion</c>
|
/// <see cref="ApplyInterpretedMovement"/>). Known residual gap: retail
|
||||||
/// directly (the SAME function
|
/// also sets the params' <c>Autonomous</c> bit (0x1000) per action (raw
|
||||||
/// <see cref="DoInterpretedMotion(uint,MovementParameters,IInterpretedMotionSink?)"/>
|
/// 305982) — not yet threaded (tracked in ISSUES, #161 follow-up note).
|
||||||
/// implements) with a fresh local <c>MovementParameters</c> per axis —
|
|
||||||
/// this wraps that shape with a throwaway params object carrying only
|
|
||||||
/// <c>Speed</c> (ctor default for everything else, matching retail's own
|
|
||||||
/// local <c>var_2c</c>).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private WeenieError DispatchInterpretedMotion(uint motion, float speed, IInterpretedMotionSink? sink)
|
private WeenieError DispatchInterpretedMotion(uint motion, float speed, IInterpretedMotionSink? sink)
|
||||||
=> DoInterpretedMotion(motion, new MovementParameters { Speed = speed }, sink);
|
=> DoInterpretedMotion(motion, new MovementParameters { Speed = speed }, sink);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// R3-W5 internal per-axis STOP primitive — the
|
|
||||||
/// <see cref="StopInterpretedMotion(uint,MovementParameters,IInterpretedMotionSink?)"/>
|
|
||||||
/// analogue of <see cref="DispatchInterpretedMotion"/>, used by
|
|
||||||
/// <see cref="ApplyInterpretedMovement"/>'s sidestep-stop/turn-stop tail
|
|
||||||
/// calls (raw 305739/305748/305770 — retail's own
|
|
||||||
/// <c>CPhysicsObj::StopInterpretedMotion</c> calls with a fresh local
|
|
||||||
/// <c>MovementParameters</c>).
|
|
||||||
/// </summary>
|
|
||||||
private WeenieError DispatchStopInterpretedMotion(uint motion, IInterpretedMotionSink? sink)
|
|
||||||
=> StopInterpretedMotion(motion, new MovementParameters(), sink);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,19 @@ namespace AcDream.Core.Tests.Input;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// R3-W6 regression suite for the edge-driven local player — specifically
|
/// 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
|
/// the "press W and stop instantly" bug (2026-07-03): the funnel's apply
|
||||||
/// pass live-read <c>InterpretedState.ForwardCommand</c> AFTER the style
|
/// pass let its own style dispatch's <c>ApplyMotion(style)</c> state-write
|
||||||
/// dispatch, whose <c>ApplyMotion(style)</c> state-write resets forward to
|
/// reset forward to Ready (raw 0051ea6c). The original W6 fix entry-cached
|
||||||
/// Ready unconditionally (raw 0051ea6c, verbatim). Retail self-heals via
|
/// the axis fields; #161 replaced that with the TRUE retail mechanism —
|
||||||
/// register-cached entry reads (proven by the 183-case live observer trace:
|
/// the apply pass's <c>MovementParameters</c> carry
|
||||||
/// the fwd dispatch carries the pre-reset command); the fix entry-caches
|
/// <c>ModifyInterpretedState = false</c> (retail's smeared bitfield store
|
||||||
/// the axes in <c>ApplyInterpretedMovement</c>.
|
/// 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
|
/// These tests bind the REAL <see cref="MotionTableDispatchSink"/> over a
|
||||||
/// return doesn't mirror the REAL <see cref="MotionTableDispatchSink"/>,
|
/// real sequencer (a fake sink's return values can mask state-write gating
|
||||||
/// which returns TRUE for the style dispatch (manager Branch 1
|
/// — the lesson that created this file).
|
||||||
/// style==target → success) — the true return is what gates the resetting
|
|
||||||
/// state-write on. These tests bind the REAL sink over a real sequencer.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class W6EdgeDrivenMovementTests
|
public class W6EdgeDrivenMovementTests
|
||||||
{
|
{
|
||||||
|
|
@ -115,7 +116,8 @@ public class W6EdgeDrivenMovementTests
|
||||||
/// trigger was <c>ApplyServerRunRate</c> (the ACE autonomous-echo tap);
|
/// trigger was <c>ApplyServerRunRate</c> (the ACE autonomous-echo tap);
|
||||||
/// V5's P1 gate drops that echo before it reaches the player, and the
|
/// 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
|
/// 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
|
/// apply_current_movement pass (HitGround re-apply, future R6 per-tick
|
||||||
/// order) still exercises. Same two statements the deleted tap ran.
|
/// order) still exercises. Same two statements the deleted tap ran.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -177,26 +177,80 @@ public class MotionInterpreterFunnelTests
|
||||||
// (0x00528240 early-accept), so it reaches the sink; the style and
|
// (0x00528240 early-accept), so it reaches the sink; the style and
|
||||||
// forward dispatches are gate-blocked (apply-only path).
|
// forward dispatches are gate-blocked (apply-only path).
|
||||||
Assert.Equal(new[] { "DIM 40000015@1.00", "STOP 6500000d" }, sink.Calls);
|
Assert.Equal(new[] { "DIM 40000015@1.00", "STOP 6500000d" }, sink.Calls);
|
||||||
// R3-W5 correction: DoInterpretedMotion's verbatim body (raw
|
// #161 correction (2026-07-03): the apply pass runs its dispatches
|
||||||
// 305575-305631) writes InterpretedState via
|
// with ModifyInterpretedState = FALSE — retail constructs var_2c
|
||||||
// InterpretedMotionState::ApplyMotion on BOTH the success path
|
// then CLEARS bits 11/14/15 (SetHoldKey / ModifyInterpretedState /
|
||||||
// (raw 305609-305610, gated on the sink's own result) AND the
|
// CancelMoveTo) and re-sets 15/17 from the args; the BN pseudo-C
|
||||||
// blocked/apply-only `label_528440` path (raw 305616-305618,
|
// smears that bitfield store into the mush expression at raw
|
||||||
// UNCONDITIONAL on the ModifyInterpretedState bit — no sink
|
// 305778 (`(word & 0x37ff) | (arg2&1)<<15 | (arg3&1)<<17`). ACE
|
||||||
// involved at all). This W5 slice is the first port to actually
|
// MotionInterp.cs:444-449 confirms independently. So NEITHER the
|
||||||
// WIRE that state-write (the pre-W5 funnel never called
|
// blocked style dispatch NOR the Falling substitution writes
|
||||||
// InterpretedMotionState.ApplyMotion from dispatch at all — state
|
// InterpretedState — the wire's forward command survives the
|
||||||
// was set only by MoveToInterpretedState's flat copy at the top of
|
// airborne pass. This is the retail landing-exit mechanism:
|
||||||
// the call), so the blocked style dispatch DOES flip
|
// HitGround's re-apply dispatches the PRESERVED command, and the
|
||||||
// ForwardCommand to Ready (0x41000003) at step 1, which then makes
|
// motion table plays the Falling→X landing link. (The previous
|
||||||
// the Falling substitution dispatch (step 2) ITself write
|
// revision of this assertion pinned 0x40000015 — the #161 bug
|
||||||
// ForwardCommand = Falling (0x40000015) via the SAME mechanism —
|
// itself: the ctor-default params let the Falling dispatch clobber
|
||||||
// this replaces the assertion this comment used to make ("state
|
// forward_command, so a stand-still landing re-dispatched Falling
|
||||||
// still flat-copies") with the verbatim end state. Only
|
// forever.)
|
||||||
// sink.Calls (dispatch ORDER) was ever cdb-verified by this file's
|
Assert.Equal(0x44000007u, mi.InterpretedState.ForwardCommand);
|
||||||
// module doc; this resulting-state assertion was an untested
|
Assert.Equal(2.85f, mi.InterpretedState.ForwardSpeed);
|
||||||
// assumption from the pre-W5 architecture.
|
}
|
||||||
Assert.Equal(0x40000015u, mi.InterpretedState.ForwardCommand);
|
|
||||||
|
[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]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue