# 2026-08-04 — Bug A / H3 diagnosis: why a remote's detected landing edge does not advance its animation **Status:** REPORT-ONLY. No source or test edits. Companion to `docs/research/2026-08-04-remote-landing-investigation.md` (hypotheses + decision table) and `docs/ISSUES.md` #32. **Capture that drives this report:** `launch-4b2.log:809-827`, six `[remote-landing]` lines for guid `0x5000000F` between `t=85680843` and `t=85748328` (67.5 s), all identical: `airborneBefore=True gravitySet=True contact=True onWalkable=True hasDefaultSink=True resolveIsOnGround=True seqStyle=0x8000003D seqMotion=0x40000015`. Four `site=per-tick`, two `site=controller`. `ACDREAM_DUMP_MOTION` was NOT enabled for this run, so there are no `VU`/`VU.land`/`UM`/`SetCycle` lines to correlate against. --- ## Headline **Retail leaves the Falling cycle on landing as a purely LOCAL consequence of the observer's own physics.** The driver is the false→true edge of the `ON_WALKABLE_TS` bit inside `CPhysicsObj::set_on_walkable` (`named symbol @0x00511310`, pseudo-C :279287), which is reached unconditionally from `CPhysicsObj::SetPositionInternal` (`@0x00515330`, :283399) on every completed transition, for every object — there is no `IsThePlayer` / `IsCreature` / ownership gate anywhere on that path. **acdream has a verbatim port of that edge** (`src/AcDream.Core/Physics/PhysicsObjUpdate.cs:137-142`) and wires it for the local player, for ordinary bodies, for placements, for spawn settle, and even for *hidden* remotes. **The one path that does not use it is the visible remote per-tick tick** — `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:471` calls `HandleAllCollisions` alone, skipping the whole `SetPositionInternal` contact/on_walkable prefix that owns the HitGround/LeaveGround edge, and substitutes a hand-rolled, **wire-latched** landing heuristic at `:493-580`. That substitution is the defect. Its two measurable consequences are named in §2 and §5. This is the same site and the same root as Bug B (ISSUES #32's 2026-08-04 addendum): the remote's `OnWalkable` bit is *asserted*, never *derived*. --- ## 1. Where a remote's animation sequence is advanced — every writer `seqMotion` is `AnimationSequencer.CurrentMotion`, a read-only mirror of `MotionState.Substate` (`src/AcDream.Core/Physics/AnimationSequencer.cs:126`; style at `:117`). `MotionState.Substate` is written in exactly two places: `src/AcDream.Core/Physics/Motion/CMotionTable.cs:323` (Branch 1, style change) and `:414` (Branch 2, cycle change), both inside `GetObjectSequence` (`:255-516`), plus the baseline install at `:622` (`SetDefaultState`). Every route into `GetObjectSequence` for a live entity: | # | Entry point | Reachable for a PLAYER remote? | |---|---|---| | W1 | `MotionTableDispatchSink.ApplyMotion/StopMotion/StopCompletely` (`src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs:34-57`) → `AnimationSequencer.PerformMovement` (`:473-477`) → `MotionTableManager.PerformMovement` (`src/AcDream.Core/Physics/Motion/MotionTableManager.cs:409-444`) | **Yes — the only live route.** | | W2 | `AnimationSequencer.SetCycle` (`:353-413`) from `RemoteServerControlledVelocityCycle.cs:78` | **No** — gated `!IsPlayerGuid(serverGuid)` at `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:181`. NPC/monster only. | | W3 | `AnimationSequencer.SetCycle` from `src/AcDream.App/Rendering/SpawnMotionInitializer.cs:34,60` | Spawn only. | | W4 | `AnimationSequencer.SetCycle`/`PlayAction` from `src/AcDream.Core/Physics/AnimationCommandRouter.cs:78,81` | Command/emote routing, not locomotion. | | W5 | `MotionTableManager.InitializeState` (`:353-362`) / `Reset` (`AnimationSequencer.cs:598-609`) | Lifecycle only. | Note: the `SetCycle` block advertised at `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:413-463` is **dead for the cycle** — `fullMotion` computed there is immediately overwritten by the funnel dispatcher's result at `:602` and used only for the locomotion-timestamp bookkeeping at `:630-638`. The comment block is stale; do not read it as a second cycle writer. **So for a player remote there is exactly ONE writer, W1, and exactly two things drive it:** - **(a) the inbound wire funnel** — `RemoteInboundMotionDispatcher.Apply` (`src/AcDream.App/Physics/RemoteInboundMotionDispatcher.cs:112`) → `MotionInterpreter.MoveToInterpretedState` (`src/AcDream.Core/Physics/MotionInterpreter.cs:2741-2788`) → `ApplyInterpretedMovement` (`:2764`). - **(b) the local ground edges** — `MotionInterpreter.LeaveGround` (`:2374-2395`) and `HitGround` (`:2426-2444`), each ending in `apply_current_movement` (`:1460-1473`) → `ApplyCurrentMovementInterpreted` (`:1547-1563`) → the **same** `ApplyInterpretedMovement` (`:2842-2903`) through the **same** `DefaultSink`. **Answer to "can anything other than an inbound `UpdateMotion` move a remote out of Falling?" — Yes, exactly one thing: `HitGround` (b).** There is no timer, no per-frame recompute, and no NPC-style velocity-cycle fallback for a player remote. If (b) is a no-op, the wire is the only escape — which is precisely the reported symptom. --- ## 2. Where the landing edge is detected, and what each site does with it Two sites, both real, neither of which silently drops the edge: **Site A — `site=per-tick`**, `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:493-580` ``` :493 if (rm.Airborne && resolveResult.IsOnGround && rm.Body.Velocity.Z <= 0f) :497 rm.Airborne = false; :502 rm.Interp.Clear(); :503-504 rm.Body.TransientState |= Contact | OnWalkable; // ASSERTED, not derived :505-506 rm.Body.Velocity = (X, Y, 0) :538-557 [remote-landing] probe :559 rm.Movement.HitGround(); :574-576 rm.Body.State &= ~Gravity; ``` **Site B — `site=controller`**, `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2019-2117` ``` :2019 if (rmState.Airborne) :2021 rmState.Airborne = false; :2023-24 rmState.Body.TransientState |= Contact | OnWalkable; // ASSERTED, not derived :2065-69 EnsureRemoteMotionBindings (creates the sink if missing) :2077-96 [remote-landing] probe :2100 rmState.Movement.HitGround(); :2114-15 rmState.Body.State &= ~Gravity; ``` Both reach `MovementManager.HitGround` (`src/AcDream.Core/Physics/Motion/MovementManager.cs:153-156`) → `MotionInterpreter.HitGround`. **The edge is not dropped at either site.** Under the captured probe values every gate in `HitGround` passes: `PhysicsObj` non-null (`MotionInterpreter.cs:2428`), `IsCreature` true (`RemoteWeenie` inherits the `IWeenieObject.IsCreature() => true` default at `MotionInterpreter.cs:462`), Gravity set (`:2435`, probe `gravitySet=True`), `Initted` true by default (`:747`). `apply_current_movement`'s dual dispatch (`:1465-1470`) routes a remote to the INTERPRETED branch because `RemoteWeenie.IsThePlayer()` is the `false` default (`:475`), and `ApplyCurrentMovementInterpreted` takes the sink branch (`:1558-1563`) because `DefaultSink` is bound (probe `hasDefaultSink=True`). **The real defect is not that the edge is dropped — it is that the edge is INVENTED.** `rm.Airborne` is an acdream latch, not retail state. It is set at exactly one place: ``` src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:1265-1273 if (update.Velocity.Z > 0.5f) { // 0xF74E VectorUpdate only rm.Airborne = true; rm.Body.TransientState &= ~(Contact | OnWalkable); rm.Body.State |= PhysicsStateFlags.Gravity; ... :1290 rm.Motion.LeaveGround(); } ``` (the other setter, `RemoteTeleportController.cs:399`, is the teleport path). So for a visible remote, **both** ground edges and **both** the Gravity bit and the contact bits are driven by one wire packet with a `0.5 m/s` magic threshold, rather than by the resolved contact plane. Retail's contact bits are derived from `contact_plane.N.z >= floor_z` on every transition (`@0x00515330`, :283501-283509) and `GRAVITY_PS (0x400)` is a persistent object property, never a per-jump latch. Two direct consequences, both citable: - **C1 — the edge cannot fire at all for an airborne episode that does not begin with a `+Z > 0.5` VectorUpdate** (walk off a ledge, step off a porch, a dropped/reordered `0xF74E`). In that case `LeaveGround` also never runs, so Falling would instead have to be engaged by the wire funnel's own airborne substitution (`MotionInterpreter.cs:2864-2868`) — which requires Gravity set, which also never happened. The remote then falls with its grounded cycle playing. - **C2 — Gravity is cleared immediately after the first landing edge** (`RuntimeRemotePhysicsUpdater.cs:574-576`, `LiveEntityNetworkUpdateController.cs:2114-2115`), whereas the local player's body is constructed with Gravity (`src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:689`) and never clears it. With Gravity clear, `contact_allows_move` returns `true` early (`MotionInterpreter.cs:2142-2143`) and `HitGround` no-ops outright (`:2435`) — so a *second* airborne episode before the next VectorUpdate has no animation response at all in either direction. Neither C1 nor C2 explains the six captured edges (all had `gravitySet=True`), but both are the same root cause and both must be fixed by the same change. --- ## 3. What retail does — LOCAL, unconditional, contact-driven Sourced from `docs/research/named-retail/acclient_2013_pseudo_c.txt`. **3.1 `CPhysicsObj::set_on_walkable` — `@0x00511310`, :279287** is the driver: ``` 00511336 if ((transient_state & 2) == 0) // OLD value not walkable 00511358 if (arg2 != 0) // -> false->true EDGE 0051135a movement_manager_1 = this->movement_manager; 00511364 MovementManager::HitGround(movement_manager_1); 00511336 else if (arg2 == 0) // true->false edge 00511346 MovementManager::LeaveGround(movement_manager); ``` The only condition is `movement_manager != 0`. **No ownership, creature, or player gate.** (`ON_WALKABLE_TS = 0x2`, `docs/research/named-retail/acclient.h:3691`.) **3.2 `CPhysicsObj::SetPositionInternal` — `@0x00515330`, :283399** calls it, unconditionally, from the resolved contact plane: ``` 00515430 if (collision_info.contact_plane_valid == 0) ts &= ~1; else ts |= 1; // CONTACT_TS 00515465 if ((ts & 1) == 0) { ts &= ~2; MovementManager::LeaveGround(...); } 0051548e else if (contact_plane.N.z < PhysicsGlobals::floor_z) set_on_walkable(this, 0); // :283507 00515482 else set_on_walkable(this, 1); // :283509 005154fe CPhysicsObj::handle_all_collisions(this, &collision_info, ts&1, ts&2); ``` `handle_all_collisions` (`@0x00514780`, :282647) does **not** touch `on_walkable` — it only does the elasticity reflect and the `frames_stationary_fall` bookkeeping. **The contact/walkable recompute lives exclusively in `SetPositionInternal`, and it runs BEFORE `handle_all_collisions`.** Wire-driven placements reach the same code: `CPhysicsObj::SetPositionInternal(Position*, SetPositionStruct*, CTransition*)` (`@0x00515bd0`, :283892) calls the transition form at `@0x00515c94` (:283942). **3.3 Retail runs full physics for objects it does not own.** `CPhysics::UseTime` (`@0x00509950`, :271586) iterates the whole object hash and calls `CPhysicsObj::update_object` on every entry (:271643); the only player-specific line is an extra `SmartBox::PlayerPhysicsUpdatedCallback` notification, not a skip. `update_object` (`@0x00515d10`, :283950) early-outs only on `parent != 0 || cell == 0 || (state & 0x1000000)` — nothing about ownership. `UpdateObjectInternal` (`@0x005156b0`, :283611) → `UpdatePositionInternal` (`@0x00512c30`, :280817) → `CPhysicsObj::transition` (`@0x005158b2`) → `SetPositionInternal` (`@0x00515914`). **So on a retail observer, a remote player's landing animation exit is a local physics consequence, not a wire event.** **3.4 The re-apply.** `MovementManager::HitGround` (`@0x00524300`, :300425) → `CMotionInterp::HitGround` (`@0x00528ac0`, :305996 — creature gate, `state & 0x400` gravity gate, `RemoveLinkAnimations`, `apply_current_movement(0,0)`) → `apply_current_movement` (`@0x00528870`, :305838; its `IsThePlayer` test is a *source selector* — raw vs interpreted — not a skip) → `apply_interpreted_movement` (`@0x00528600`, :305713): ``` 0052866c if (contact_allows_move(this, interpreted_state.forward_command) == 0) 005286ee DoInterpretedMotion(this, 0x40000015, &var_2c); // :305729 FALLING 0052866c else 00528687 DoInterpretedMotion(this, interpreted_state.forward_command, ...); // :305744 ``` `contact_allows_move` (`@0x00528240`, :305471) returns 1 iff `CONTACT_TS && ON_WALKABLE_TS` (given a gravity-bound creature). **`0x40000015` is dispatched from exactly one site in the entire 1.4 M-line binary** — that one line. Entry to and exit from Falling are both decided by the contact bits. **3.5 There is no other landing path.** `symbols.json` contains no `land`/`land_on_ground` animation function (only `CSphere::land_on_sphere` `@0x005379A0` and `CCylSphere::land_on_cylinder` `@0x0053B3D0`, both collision geometry). The complete caller set of `apply_current_movement` is `HitGround` (`@0x00528af7`), `LeaveGround` (`@0x00528b66`), `set_hold_run` (`@0x00528b9e`), `SetHoldKey` (`@0x00528bd4`/`@0x00528bef`), `ReportExhaustion` (`@0x005288ed`), `SetWeenieObject` (`@0x00528955`), and `@0x00528a08`. Landing is `HitGround` and nothing else. **3.6 The retail default that matters:** `InterpretedMotionState::InterpretedMotionState` (`@0x0051e8d0`, :293418) sets `forward_command = 0x41000003` (Ready). A remote that never received an mt-0 `UpdateMotion` still re-applies **Ready** on landing, never Falling. acdream matches this (`src/AcDream.Core/Physics/MotionInterpreter.cs:215-224`). --- ## 4. Does the local player differ? Yes — and that is the shape of the fix **The local player implements retail's contact-derived edge inline.** `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:2607-2645`: ``` :2608 if (resolveResult.Ok && candidateMoved) { :2610-13 Contact <- resolveResult.InContact :2616 if (resolveResult.InContact && resolveResult.OnWalkable) { :2618 bool wasAirborne = !_body.OnWalkable; // read BEFORE the write :2619 _body.TransientState |= OnWalkable; :2620-27 if (wasAirborne) { Movement.HitGround(); landedThisQuantum = true; } :2630-33 } else { _body.TransientState &= ~OnWalkable; } :2641-44 PhysicsObjUpdate.HandleAllCollisions(...); // AFTER the edge, as retail :2650-51 if (!_body.OnWalkable && !_wasAirborneLastFrame) _motion.LeaveGround(); ``` That is `set_on_walkable`'s edge, derived from the resolved contact plane, with `handle_all_collisions` in retail's order. **Four other acdream paths already use the extracted seam:** | Path | Call | |---|---| | Local player, hidden/PositionManager | `PlayerMovementController.cs:2044-2053` → `CommitSetPositionTransition(..., Movement.HitGround, _motion.LeaveGround)` | | Ordinary live bodies | `src/AcDream.Runtime/Physics/RuntimeOrdinaryPhysicsUpdater.cs:168` | | Canonical placements (incl. remotes) | `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs:4912-4919` → `..., remote.HitGround, remote.LeaveGround, guard.IsCurrent` | | **Hidden remotes** | `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:778-796` → `CommitSetPositionTransition(..., rm.Movement.HitGround, rm.Motion.LeaveGround, ...)`, then `:796 rm.Airborne = !rm.Body.OnWalkable;` | | Spawn settle | `src/AcDream.Core/Physics/SpawnPlacementSettler.cs:62` | The seam is `src/AcDream.Core/Physics/PhysicsObjUpdate.cs:120-151`: ``` :131 bool finalOnWalkable = CommitSetPositionContactPrefix(body, inContact, onWalkable, previousOnWalkable); :137 if (!previousOnWalkable && finalOnWalkable) hitGround?.Invoke(); :143 else if (previousOnWalkable && !finalOnWalkable) leaveGround?.Invoke(); ``` with `finalOnWalkable = inContact && onWalkable` at `:169` and `IsWalkableContact(inContact, n) => inContact && n.Z >= PhysicsGlobals.FloorZ` at `:20-21` — a verbatim port of :283501-283509. **The VISIBLE remote per-tick path is the only one that bypasses it.** `RuntimeRemotePhysicsUpdater.cs:471-477` calls `PhysicsObjUpdate.HandleAllCollisions` directly — the *tail* of `SetPositionInternal` without its *prefix* — so the remote's `Contact`/`OnWalkable` bits are never derived from the resolve, the false→true edge never exists, and `HitGround`/`LeaveGround` have no driver. The `rm.Airborne` latch at `:493` is the stand-in. **So yes: the same mechanism is simply not wired for visible remotes, and it is already wired 300 lines below in the same file for hidden ones.** The fix is small. --- ## 5. Proposed fix ### 5.1 The change (H3's file, per the investigation doc's three-file split) The investigation doc says H1's fix is in the Gravity-clear sites, H2's is in the sink-binding race, and **H3's is "the animation-scheduler consumption path, not physics."** That framing needs one correction, which this trace establishes: the consumption path is fine (see §5.3) — H3's actual file is **`src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`**, the *producer* of the animation command, not `LiveEntityAnimationScheduler`/`Presenter`. **Target: `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:471-580`.** Replace the direct `HandleAllCollisions` call at `:471-477` **and** the entire hand-rolled landing block at `:493-580` with the same call the hidden path already makes at `:778-796`: ```csharp // retail CPhysicsObj::SetPositionInternal @0x00515330 (:283501-283509) -> // set_on_walkable @0x00511310 (:279287): contact/on_walkable derived from the // contact plane, HitGround/LeaveGround on the false<->true edge, then // handle_all_collisions @0x00514780. Same order, same seam as the hidden // remote path below and the local player at PlayerMovementController:2607. if (!PhysicsObjUpdate.CommitSetPositionTransition( rm.Body, resolveResult.InContact, resolveResult.OnWalkable, // DERIVED, not asserted resolveResult.CollisionNormalValid, resolveResult.CollisionNormal, previousContact, previousOnWalkable, rm.Movement.HitGround, rm.Motion.LeaveGround, () => IsCurrentOwner(record, rm, objectClockEpoch, externalOwnerValid))) { return false; } rm.Airborne = !rm.Body.OnWalkable; // derived state, not a wire latch ``` Three supporting deletions/changes fall out of it, all required for the seam to be the single owner: 1. **Delete the unconditional `TransientState |= Contact | OnWalkable`** at `:152-154` (the `!rm.Airborne` per-tick force) — it is what makes the edge structurally impossible. This is also Bug B's forced-`OnWalkable` (ISSUES #32 addendum), so the two bugs close together. 2. **Delete the twin landing block** at `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2019-2117`'s `TransientState |= Contact | OnWalkable` + `HitGround` + Gravity-clear, and the `rm.Airborne = true` + contact-clear + Gravity-set at `:1265-1273`. `LeaveGround` at `:1290` goes with them — retail fires it from `set_on_walkable`, not from a `0xF74E` handler. The VectorUpdate keeps only what retail's `PhysicsDesc` write does: install the wire velocity. 3. **Make Gravity persistent on remote bodies**, matching `PlayerMovementController.cs:689`: construct `RemoteMotion.Body` with `PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions` (`src/AcDream.Runtime/Physics/RemoteMotion.cs:278`) and delete both Gravity-clear sites (`RuntimeRemotePhysicsUpdater.cs:574-576`, `LiveEntityNetworkUpdateController.cs:2114-2115`). Retail's `GRAVITY_PS` (`0x400`) is an object property, and both `HitGround` (`:2435`) and `contact_allows_move` (`:2142`) read it as one. Without this, C2 (§2) survives the fix. This is *not* a symptom-site guard: it deletes the invented mechanism and installs the retail one that the rest of the codebase already uses. ### 5.2 Blast radius - **Player remotes** — the intended target. Landing/leaving-ground animation becomes local and unconditional, as retail. Also removes the `0.5 m/s` magic threshold and the wire dependency for the whole ground-edge family. - **NPCs / monsters** — same code path (`#184` Slice 2b unified them; there is no player/NPC fork in `Tick`). They gain a correct `LeaveGround` when they walk off a ledge, which they do not have today. Their separate stale-velocity cycle stop (`RuntimeRemotePhysicsUpdater.cs:181-192` → `RemoteServerControlledVelocityCycle.cs:78`) is untouched. - **Local player** — untouched. It already does this inline (`PlayerMovementController.cs:2607-2645`). - **Projectiles** — untouched. They use `ProjectilePhysicsStepper.cs:374` (`ApplySetPositionContact`) and are excluded from this branch by `projectileHandlesMovement` (`LiveEntityAnimationScheduler.cs:336`). - **Hidden remotes / placements / spawn settle** — untouched; they already call the seam. - **Bug B** — expected to change behavior at the same commit, because a steep roof stops being force-marked `OnWalkable`. §5.5 covers the coupling. - **Register bookkeeping** — this retires the "forced Contact|OnWalkable on remote landing" deviation and the `rm.Airborne` wire-latch deviation, and it touches AD-25's call-site note at `RuntimeRemotePhysicsUpdater.cs:449-470` (the comment claims the direct `HandleAllCollisions` call matches "retail's own unconditional call site" — it does not, because retail's call site is *inside* `SetPositionInternal`, after `set_on_walkable`). Per CLAUDE.md the register rows must be updated in the same commit. ### 5.3 What this fix does NOT do — and why the scheduler is exonerated The animation-scheduler consumption path was H3's literal wording. It is clean: - `LiveEntityAnimationScheduler.cs:323-326` advances the sequencer and captures the pose, **then** `:351-364` runs `_remotePhysics.Tick` (where `HitGround` fires). So a cycle change lands in the *next* quantum's pose — a one-frame lag, not a multi-second one. - `LiveEntityMotionRuntimeController.cs:39-47` creates `rm.Sink` once over `ae.Sequencer` and binds it as `rm.Motion.DefaultSink` (`:46`). The **same** cached sink is handed to the inbound funnel at `LiveEntityNetworkUpdateController.cs:756,766`. A stale-sequencer theory is therefore refuted by observation: if the sink pointed at a dead sequencer, the wire path could not clear the pose either — and the user reports it does. - `RemoveLinkAnimations` is bound to `sequencer.Manager.HandleEnterWorld` (`LiveEntityMotionRuntimeController.cs:63`), which is the correct `#174` binding. ### 5.4 Second, independent hazard found on the way — flag, do not fix blind `MovementParameters.ModifyInterpretedState` defaults to **true** (`src/AcDream.Core/Physics/Motion/MovementParameters.cs:140`), and `MoveToInterpretedState`'s action-replay loop (`MotionInterpreter.cs:2766-2784`) dispatches with ctor-default params (`DispatchInterpretedMotion`, `:3222-3225`). `InterpretedMotionState.ApplyMotion` (`:285-313`) writes `ForwardCommand = motion` for **any** id carrying `0x40000000` (`:299-304`) — which includes `Falling` (`0x40000015`). `InboundInterpretedMotionFactory.Create` (`src/AcDream.App/Physics/InboundInterpretedMotionFactory.cs:46-63`) restores the real class byte via `MotionCommandResolver.ReconstructFullCommand`, whose catalog is built from the DatReaderWriter `MotionCommand` enum — so a wire `Commands[]` entry of `0x0015` would resolve to `0x40000015`. If that ever happens, `InterpretedState.ForwardCommand` becomes `Falling`, and `HitGround`'s re-apply at `MotionInterpreter.cs:2878` re-dispatches Falling — producing **exactly** "stuck in Falling until the next motion update overwrites ForwardCommand." This mechanism would survive the §5.1 fix. **NOT ESTABLISHED:** whether ACE ever puts a `0x4x`-class command in `Commands[]` for a jumping/falling player. Retail's own outbound cannot (its `ModifyInterpretedState=false` invariant keeps Falling out of `interpreted_state`), but ACE is an emulator. **Settle it with one capture:** `ACDREAM_DUMP_MOTION=1 ACDREAM_REMOTE_VEL_DIAG=1` across a remote jump, and read the `[UM_RAW]`/`[FWD_WIRE]` lines (`LiveEntityNetworkUpdateController.cs:373-386`, `:777-786`) for a `ForwardCommand` or `Commands[]` entry of `0x0015` during the airborne window. ### 5.5 Sequencing note Bug B's addendum (ISSUES #32) records that correcting `OnWalkable` alone may not reproduce retail's roof slide, because it also depends on **#173**'s remote collision-velocity reflect (shipped, gate unrun) and **AD-10**'s terrain-only slope projection. That does not block this fix — the animation edge is independent of the slide response — but expect the roof case to change appearance at the same commit, and gate both together. --- ## 6. How to test ### 6.1 The measurement that is still missing **The existing probe reads the wrong side of the call.** It captures state immediately *before* `HitGround` (`RuntimeRemotePhysicsUpdater.cs:538-557`, `LiveEntityNetworkUpdateController.cs:2077-2096`), and the investigation doc already flagged for H3 that "the read happens immediately before HitGround runs, so this alone doesn't distinguish success from H3." The six captured lines therefore prove the gates pass and prove nothing about the outcome. Before or alongside the fix, one line of instrumentation settles it: emit a second `[remote-landing-after]` line immediately after `rm.Movement.HitGround()` carrying `seqMotion`, `InterpretedState.ForwardCommand`, and the `MotionTableManagerError` returned by the sink's `ApplyMotion`. Three outcomes, three different conclusions: | post-`seqMotion` | `ForwardCommand` | Conclusion | |---|---|---| | Ready (`0x41000003`) | Ready | The edge works; the residual is elsewhere (re-verify the user's observation) | | Falling (`0x40000015`) | Falling | §5.4 — the wire clobbered `ForwardCommand` | | Falling | Ready, sink returned `0x43` | `CMotionTable.IsAllowed` (`CMotionTable.cs:172-188`) refused the Ready cycle because its `MotionData.Bitfield & 2` is set — a DAT fact, not a source fact | ### 6.2 Automated New tests in `tests/AcDream.Runtime.Tests/Physics/` (the layer under test): 1. **Edge from contact, not from the wire.** Drive `RuntimeRemotePhysicsUpdater.Tick` with a stubbed resolve returning `InContact=false, OnWalkable=false` then `InContact=true, OnWalkable=true`, with **no** VectorUpdate ever delivered. Assert `HitGround` fired exactly once on the transition and `LeaveGround` exactly once on the opposite one. This fails today (C1). 2. **Steep contact does not land.** Resolve returns `InContact=true` with a contact normal whose `Z < PhysicsGlobals.FloorZ`. Assert `OnWalkable` stays clear and `HitGround` did **not** fire. This is the Bug B assertion at the same site. 3. **Idempotence.** Two consecutive grounded quanta fire `HitGround` once, not twice. 4. **Gravity persistence.** After a landing, assert `rm.Body.State.HasFlag(PhysicsStateFlags.Gravity)` — then run a second airborne→grounded cycle and assert `HitGround` fires again. This fails today (C2). 5. **End-to-end cycle exit.** With a real `MotionTableDispatchSink` over a test `AnimationSequencer` seeded to `Substate = Falling` and `InterpretedState.ForwardCommand = Ready`, assert `CurrentMotion` leaves `0x40000015` after the grounded commit. This is the assertion that makes the §6.1 table unnecessary going forward. 6. **Regression guard on the existing contract:** `tests/AcDream.Core.Tests/Physics/MotionInterpreterFunnelTests.cs:201` (`HitGround_AfterFall_RedispatchesPreservedForward_ExitsFalling`) already covers the interp layer with a fake sink and must stay green. ### 6.3 What the user would see Two-client route 4a, retail driving `+Acdream`'s neighbour: - **Jump on flat ground** — the falling pose clears the instant the remote's feet touch, with no wire round-trip and no wait for the next `UpdateMotion`. - **Walk off a low ledge / porch step** (no jump packet at all) — the remote now plays the falling cycle on the way down and exits it on landing. Today it keeps its grounded cycle the whole way, because `rm.Airborne` never latches. - **Two jumps in quick succession** — the second one animates identically to the first (today the Gravity clear can mute it). - **Jump onto a house roof (Bug B)** — the remote should now slide rather than plant, subject to the #173 / AD-10 coupling in §5.5. Gate this together with Campaign P matrix scenario 8. --- ## NOT ESTABLISHED 1. **Which of the two terminal failures actually fires at the captured edges.** The probe measures before the call. §6.1 names the exact one-line instrumentation and the three-way decision table. Everything upstream of the sink call is established: all gates pass, the sink is bound, and the dispatch target is `InterpretedState.ForwardCommand`. 2. **Whether ACE emits a `0x4x`-class motion command for a falling player** (§5.4). Settled by `ACDREAM_DUMP_MOTION=1` + `ACDREAM_REMOTE_VEL_DIAG=1` across a remote jump. 3. **Whether the humanoid MotionTable's `Ready` cycle carries `MotionData.Bitfield & 2`.** This is DAT content, not source. If set, `CMotionTable.IsAllowed` (`:172-188`, retail `is_allowed @0x005226c0`, :298526) refuses a Ready cycle requested while the substate is Falling, because Falling is not any style's default substate. Observation argues against it — the wire path uses the identical `GetObjectSequence` call and does clear the pose — but it is not proven. Settle by dumping `MotionData.Bitfield` for cycle key `(style << 16) | 0x000003`, or with `bp acclient!CMotionTable::is_allowed` reading `[edx+0x30]`. 4. **Whether ACE sets `GRAVITY_PS (0x400)` in the broadcast `PhysicsDesc` for remote creatures.** Relevant only to how faithfully §5.1's item 3 should be implemented (construct-with-Gravity vs adopt-from-wire). cdb: `bp acclient!CMotionInterp::contact_allows_move` dumping `physics_obj->state`.