diff --git a/docs/research/2026-07-03-r4-verify-session-handoff.md b/docs/research/2026-07-03-r4-verify-session-handoff.md new file mode 100644 index 00000000..63e18776 --- /dev/null +++ b/docs/research/2026-07-03-r4-verify-session-handoff.md @@ -0,0 +1,98 @@ +# R4 live-verify session handoff — fresh-session entry point (2026-07-03, evening) + +Successor to `2026-07-03-phase-r-session-handoff.md` (whose postscripts +summarize the same arc — this doc is the current entry point). Worktree +`vigorous-joliot-f0c3ad`, branch `claude/vigorous-joliot-f0c3ad`, tree CLEAN +at `3c866f95`. Full suite green: **3,963** (+15 since the R4 handoff). + +## State + +**R4 is SHIPPED and user-verified live** (V0–V6 + five root-cause fixes the +live pass exposed). Confirmed working by the user's eyes: local use-walk +(turn-first, walk/run by distance, arrival, door opens once), door open + +door SWING animation, remote (retail-client) close-range moveto walking, +and remote run-distance movetos at correct animation pace AND speed (#160). + +## The five live fixes (trail, newest first — each is a real mechanism, no adaptations added) + +| Commit | Root cause fixed | +|---|---| +| `41006e79` | **#160 slow-motion remote runs**: remote interps had NO weenie → retail's `apply_run_to_command` rate chain (`weenie ? (InqRunRate() ?: my_run_rate) : 1.0`, raw 305062-305076) took the degenerate 1.0 branch; the wire's `MoveToRunRate` (stored in `MyRunRate` by the mt-6/7 unpack, M13) was never consumed. Fix: `RemoteWeenie` (Core) — retail's per-object ACCWeenieObject stand-in; `InqRunRate` FAILS → `my_run_rate` fallback; `IsThePlayer`=false ends the "null weenie counts as the player" A3 reading. | +| `350fb5e3` | **Door-swing snap** (register TS-40): CMotionInterp's detached-object link-strip guard (`physics_obj->cell == 0`, raw @305627) was proxied by `CellPosition.ObjCellId==0`, seeded only by the local player's `SnapToCell` → every REMOTE body read "detached" and every dispatched transition link was stripped same-tick. Fix: explicit `PhysicsBody.InWorld` set by SnapToCell + RemoteMotion construction. | +| `006cf659` | **Remote-player glide (partial)** + **door UMs dropped**: (a) remote PLAYERS' MoveToManagers were never ticked (the V4 UseTime slot was NPC-only) — extracted `TickRemoteMoveTo`, now also in the grounded player-remote (L.3 M2) pipeline; (b) UP-less entities (doors) never got a RemoteMotion → since the S2b funnel cutover their UM motions were parsed and dropped — rm now created on first UM, with a `LastServerPosTime>0` gate keeping UP-less bodies OUT of the dead-reckoning tick. | +| `24569fd2` | **Moveto stall #2**: login `SetPosition` ran before the DefaultSink bind in `EnterPlayerModeNow` → its StopCompletely A9 node had no completion partner → ONE immortal pending_motions node → wait-for-anims gate never opened. Fix: bind block moved above SetPosition; `LoginQueue_DrainsToEmpty_UnderProductionFeed` pins the invariant. | +| `c2dc1a88` | **Moveto stall #1 (the big one)**: the R3 port MISREAD `StopCompletely_Internal` (0x0050ead0) as a velocity zero — it is `CPartArray::StopCompletelyInternal` (0x00518890) = `MotionTableManager::PerformMovement(type 5)`, the ANIMATION-side stop whose UNCONDITIONALLY-queued entry is the matched pop for the A9 pending_motions node. Fix: `IInterpretedMotionSink.StopCompletely()` → the R2 manager's already-ported type-5 op. Also: retail's per-tick `CheckForCompletedMotions` slot (CPartArray::HandleMovement 0x00517d60) added to the controller tick, and the P1 autonomous-STORE family completed (unpack store 00509730, input-edge stores DoMotion@00510030/StopMotion@005100e0, section-2 stops per-frame stamping, speculative install stores false). | + +## The queue model (the load-bearing mental model — do not relearn) + +`pending_motions` (interp) pops HEAD-ANY on each `MotionDone`; completions +come from the manager's `pending_animations` entries. **Every enqueue must +have a completion partner** (dispatch → manager entry). One orphan = +permanent ≥1 backlog (later completions just relabel it) = `MotionsPending` +true forever = `BeginTurnToHeading`'s verbatim wait-for-anims gate wedges +every moveto. Orphan producers found: StopCompletely-before-sink-bind and +the misread StopCompletely_Internal. The `[autowalk-gate]` probe prints the +whole gate state — it is how both wedges were pinned. + +## OPEN — next session picks up here (full trails in docs/ISSUES.md) + +1. **#161 — remote jump landing stuck in the falling pose** (retested + post-TS-40: still broken). Evidence in the issue: `VU.land` fires but + the first post-landing UM still sees `seq now motion=0x40000015`; the + char recovers only via subsequent wire UMs; window-focus change also + unsticks it (a frame-advance/big-dt hint). PRIME SUSPECT: the funnel's + airborne branch dispatches Falling via `DoInterpretedMotion` with + ctor-default `ModifyInterpretedState=true` → interpreted fwd BECOMES + 0x40000015 → `HitGround`'s `apply_current_movement` (interpreted branch + for remotes) re-dispatches FALLING instead of the pre-fall wire command. + CHECK RETAIL FIRST: does raw ~305730's falling dispatch really write + interpreted fwd; what exits Falling on a retail REMOTE landing + (HitGround raw @305949, the `apply_current_movement(this, 1, ...)` + arg semantics); does K-fix17's landing SetCycle even execute (probe it). +2. **#162 — glide-class adjudication (PENDING USER)**: ACE re-broadcasts + every mover MoveToState to observers as autonomous mt-0 + (MovementData.cs:162 — ACE's own "this is a hack" comment; + GameActionMoveToState.cs:36 unconditional; chain sends ONE mt-6, no + re-emit, nothing on arrival — all ACE-source-verified, see the issue). + Retail-verbatim head-interrupt cancels the observer moveto on each. + The user's retail-observing-retail A/B decides: parity (accept or a + NARROW registered adaptation — **explicit user approval required**) vs + new fact. +3. **#163 — strip the TEMPORARY diagnostics** once #161/#162 close: + `[autowalk-gate]` + `_lastAutowalkGateLogTime` (PlayerMovementController) + and `[autowalk-feed]` (GameWindow player tracker feed). +4. Then the **combined R2+R3+R4 visual-pass remainder** (jump/ledge + momentum, run-in-circles blend, stop settle, retail-observer view of + +Acdream) and **R5** (MovementManager facade + StickyManager + + ConstraintManager + full TargetManager — retires TS-39 + AP-79's + adapter). + +## Session gotchas worth keeping + +- **Evidence-first**: two plausible-but-wrong theories (a per-tick + apply_current_movement "pump" — retail's apply callers are EVENT-driven + only; and "ACE chain broadcasts mt-0 locomotion" — it doesn't) were + killed by the raw + wire captures. The `[autowalk-gate]` probe and the + UM raw hex dumps (autonomy byte at offset +14) were the decisive tools. +- **Retail object-model completeness matters**: two of the five fixes were + "retail gives every placed object X and we only gave it to the player" + (a weenie; a cell/InWorld). When a dispatch path behaves differently for + remotes, check what retail's CPhysicsObj carries that our RemoteMotion + lacks BEFORE suspecting the ported logic. +- The launch recipe + graceful-close rules are unchanged (CLAUDE.md); the + client locks builds — ask the user to close it, never kill. +- Register: TS-40 added; TS-33 extended (orientation-diff gap); AP-79 + covers both tracker instances; TS-36/AD-26 retired earlier today. + +## Key files this session touched + +`MotionInterpreter.cs` (StopCompletely sink dispatch, InWorld guards), +`MoveToManager.cs` (MoveToComplete widened to BeginNextNode completion), +`MotionTableDispatchSink.cs` + `InterpretedMotionFunnel.cs` +(StopCompletely sink op), `PhysicsBody.cs` (InWorld), `RemoteWeenie.cs` +(new), `PlayerMovementController.cs` (UseTime slot + per-tick Check + +edge autonomy stores + temp probe), `GameWindow.cs` (P1 gate + store, +RouteServerMoveTo, TickRemoteMoveTo, rm-on-UM, EnterPlayerModeNow bind +order, player tracker feed), tests: `PlayerMoveToCutoverTests`, +`MoveToManagerCompletionSeamTests`, `InWorldLinkGuardTests`, +`RemoteWeenieRunRateTests`, `W6EdgeDrivenMovementTests` (apply-pass form).