# 2026-08-04 — Bug B (remote ledge/roof slide) diagnosis **Status:** REPORT-ONLY. No source or test edits made. HEAD `7f1c1f5a`. **Companion:** `docs/research/2026-08-04-remote-landing-investigation.md` (Bug A), `docs/ISSUES.md` #32 (2026-08-04 addendum), divergence register rows **AP-87**, **AP-81**, **AD-10**. **Relationship to the prior same-day investigation.** ISSUES.md #32 already carries a 2026-08-04 root-cause addendum naming the landing block's unconditional `Contact | OnWalkable`. That addendum is **correct but incomplete**: it names one of *four* independent links in the chain, and the one it names is not the dominant one. Fixing only the landing block cannot produce a slide. §1 below establishes the full chain; §6 states what that means for the fix. Everything here is re-verified against source and against the pseudo-C directly — nothing is carried over on trust. --- ## 1. Does acdream locally simulate remote bodies, and would it slide one off a steep roof? **It simulates them, and it cannot slide them. Four independent links each block the slide on their own.** The per-tick remote owner is `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`, `Tick(...)` (`:61-650`). It runs once per eligible remote per retail object quantum and *does* call `PhysicsEngine.ResolveWithTransition` (`:370-418`) — the same sweep the local player uses. So the naive framing ("acdream only applies wire positions") is **wrong**: there is a real per-tick sweep. The problem is that everything fed into that sweep has been pre-flattened. ### Link 1 — the grounded branch force-sets `Contact | OnWalkable` every tick, unconditionally `RuntimeRemotePhysicsUpdater.cs:150-154`: ```csharp if (!rm.Airborne) { rm.Body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable | TransientStateFlags.Active; ``` The gate is `rm.Airborne` — an acdream client-side bool — **not** the contact plane and not the wire bit. This is the dominant site: it runs *before every sweep, on every tick*, so it re-asserts the walkable lie even if some other site cleared it. The landing block that ISSUES.md #32 names (`LiveEntityNetworkUpdateController.cs:2023-2024`) and its per-tick twin (`RuntimeRemotePhysicsUpdater.cs:503-504`) fire once each per landing; this one fires ~30 times a second forever. ### Link 2 — a grounded remote's velocity is zeroed every tick `RuntimeRemotePhysicsUpdater.cs:169`: ```csharp rm.Body.Velocity = System.Numerics.Vector3.Zero; ``` There is nothing left to slide *with*. This also silently discards any authoritative velocity ACE delivered: `OnVector` (0xF74E) writes `update.Velocity` into the body via `TryCommitAuthoritativeVector` (`LiveEntityNetworkUpdateController.cs:1250-1258`), but a downhill slide has `Velocity.Z < 0`, so the `update.Velocity.Z > 0.5f` test at `:1265` leaves `rm.Airborne == false`, and the next tick's `:169` erases the vector. ### Link 3 — the Gravity state bit is cleared at landing and never restored `RuntimeRemotePhysicsUpdater.cs:571-576` and `LiveEntityNetworkUpdateController.cs:2110-2116` both do `rm.Body.State &= ~PhysicsStateFlags.Gravity` after `HitGround()`. `PhysicsBody.calc_acceleration()` (`PhysicsBody.cs:503-518`) returns `Acceleration = Vector3.Zero` when the Gravity bit is clear. The bit is only ever *set* by the jump VectorUpdate (`:1273`) and once at body construction (`RuntimeRemoteBodyDescription.cs:221` / `RuntimePhysicsState.InitializeNewPhysicsBody`, `RuntimePhysicsState.cs:2617`), and that initializer runs **once per incarnation** ("Later SetState never replays them", `RuntimePhysicsState.cs:2608-2612`). So after the first landing the remote has no gravity for the rest of its life. Retail does the opposite: GRAVITY_PS is a persistent object property and gravity *acceleration* is gated on the CONTACT/ON_WALKABLE transients inside `calc_acceleration`, not on the state bit being toggled. This is already filed as register row **AP-81** — but AP-81's file:line column lists only the VectorUpdate handler and the landing blocks, not `:150-154` (Link 1). ### Link 4 — the visible remote tick never commits the sweep's own contact classification The engine *does* compute the retail answer. `PhysicsEngine.cs:2653-2676`: ```csharp bool inContact = ci.ContactPlaneValid; bool onWalkable = PhysicsObjUpdate.IsWalkableContact(inContact, ci.ContactPlane.Normal); bool onGround = inContact || (transition.ObjectInfo.State & ObjectInfoState.OnWalkable) != 0; ``` and `IsWalkableContact` (`PhysicsObjUpdate.cs:20-21`) is `inContact && contactNormal.Z >= PhysicsGlobals.FloorZ` — retail-shaped. Both `InContact` and `OnWalkable` are carried out on `ResolveResult` (`ResolveResult.cs:46-53`). `Tick` reads **only** `resolveResult.Position`, `.CellId`, `.IsOnGround`, `.CollisionNormalValid`, `.CollisionNormal` (`:420-477`). It never reads `.InContact` or `.OnWalkable`, and — unlike `TickHidden`, which calls `PhysicsObjUpdate.CommitSetPositionTransition` at `:778-795` — it never commits them to the body. `PhysicsEngine` itself writes `ContactPlane*`, `WaterContact`, `Sliding`, and the Stationary bits back onto the body, but **not** `Contact`/`OnWalkable` (grep of `PhysicsEngine.cs` for `body.TransientState`: `:2488-2540` only). Worse, `IsOnGround` is `inContact || …` — so a **steep-roof contact reports `IsOnGround == true`**. That is the value the airborne→grounded landing detection at `:493-495` tests. The remote therefore "lands" on a surface retail would classify as contact-but-not-walkable, and the landing block then forces `OnWalkable` and clears Gravity. **Conclusion for §1:** a remote resting on a steep roof is held there by acdream's own physics, not merely parked at a wire position. The sweep runs every tick and returns "no movement" because the mover has zero velocity, zero acceleration, and a forged walkable contact. --- ## 2. What produces the blip **Two candidates, both live, and they are distinguishable only by capture.** The first is the one AP-87 predicted; the second is a retail-faithful mechanism firing correctly on a body that has been frozen by §1 — and it fits the user's timing description ("stay there *briefly*, then blip") better. ### Candidate 1 — AP-87's `bodyToTarget > 4 m` snap `src/AcDream.Runtime/Physics/RuntimeRemoteSteadyStatePosition.cs:129-137`: ```csharp bool firstUp = remote.LastServerPosTime <= 0.0; float bodyToTarget = Vector3.Distance(remote.Body.Position, worldPosition); if (firstUp || !willBeDrTicked || bodyToTarget > BodySnapThreshold) // BodySnapThreshold = 4f, :42 { remote.Interp.Clear(); remote.Body.Position = worldPosition; remote.Body.Orientation = orientation; return Action.Snapped; } ``` This requires the server position to have descended more than 4 m from the parked body. It only fires on a packet that classifies `Interpolate`. ### Candidate 2 — the InterpolationManager's own stall blip (`node_fail_counter > 3`) `src/AcDream.Core/Physics/InterpolationManager.cs:405-467` ports retail's 5-frame stall window and its snap-to-tail: ```csharp if (_frameCounter >= StallCheckFrameInterval) // 5 frames { float cumulative = _originalDistance - dist; // progress made … if (!primaryPass && !secondaryPass) _failCount++; // no progress -> fail else _failCount = 0; } … if (_failCount > StallFailCountThreshold) // > 3, :458 { InterpolationNode tail = _queue.Last!.Value; Vector3 tailDelta = tail.TargetPosition - currentBodyPosition; Clear(); return new InterpolationStep(true, tailDelta, tail.TargetOrientation); } ``` This is a faithful port of retail `InterpolationManager::UseTime` **@0x00555f20** (pseudo-C :353261): `if (node_fail_counter > 3)` → `SetPositionSimple(physics_obj, , 1)` @0x0055605D / @0x00556021, then `StopInterpolating` @0x00556061. Verified directly. A body frozen by §1 makes **zero** progress every window, so `_failCount` increments on every 5th frame and crosses 3 after ~20 frames — **about 0.33 s at 60 Hz**. That is "stay there briefly, then blip", and the blip lands exactly at the tail waypoint, i.e. the server's already-slid-down position. The code is not wrong. Retail's own remote would never trip this, because retail's remote is genuinely sliding and therefore genuinely making progress. **This is a downstream symptom of §1, not an independent defect — do not touch `InterpolationManager`.** ### Elimination of the remaining candidates | Candidate | Site | Verdict | |---|---|---| | Far snap (≥96 m) — route 4b-2, just landed at `7f1c1f5a` | `RuntimeRemoteFarSnapPosition.ResolveArm`; classifier `RuntimeAuthoritativePositionRouteClassifier.cs:454-459` (`nearby = PlayerDistance < 96f`) | **Ruled out.** The observer is watching the house; `player_distance` is far under 96 m, so the classification is `Interpolate`, never `SetPositionSimple`. | | `RemoteContactArm.AirborneSnap` | `LiveEntityNetworkUpdateController.cs:1031-1049` | Ruled out for the *post-plant* blip: it requires `remote.Airborne`, which is false once the body has planted (cleared at `:2021` / `:497`). It *is* the mechanism for the initial plant-onto-the-roof snap. | | Airborne no-op branch | `LiveEntityNetworkUpdateController.cs:1994-1998` / `:2273-2280` | Not a writer at all — it writes nothing but `CellId` + `LastServerPos` (AP-135). It is a *contributor* (see below), not the blip. | | Teleport / `ForcePosition` | `RemoteTeleportController` | Ruled out: requires a bumped teleport/force-position sequence, which ACE does not emit for ordinary sliding movement. | | `InterpolationManager` 100 m autonomy blip | — | Ruled out at this distance. | ### The un-established half: what the wire says during the slide There are **two** shapes of the same symptom, and code alone cannot tell them apart: - **Shape A** — ACE reports `IsGrounded == false` throughout the retail sender's slide. ACE derives that flag from its *own* server-side physics: `references/ACE/Source/ACE.Server/Network/Structure/PositionPack.cs:72-73`, `if ((PhysicsObj.TransientState & TransientStateFlags.OnWalkable) != 0) flags |= PositionFlags.IsGrounded;`. A steep roof is not walkable, so if ACE's server physics classifies it the same way retail does, the whole slide arrives with the bit clear. Every one of those packets classifies `NoPositionOperation` (`RuntimeAuthoritativePositionRouteClassifier.cs:422-443`) and acdream writes **nothing** — not even the interpolation queue. The remote is frozen for the entire slide, and the first `IsGrounded == true` packet after the sender reaches walkable ground fires the 4 m snap. Visible as: *land, total stillness, one large blip.* - **Shape B** — ACE reports `IsGrounded == true` throughout. Each packet classifies `Interpolate` and feeds the queue. The body still cannot move (§1), so **Candidate 2 fires first**, at ~0.33 s, snapping to the tail waypoint; Candidate 1 would only get its turn if the queue were empty. Visible as: *land, ~third of a second, blip.* Note that in **Shape A** the queue is never fed at all (the `NoPositionOperation` branch writes nothing), so Candidate 2 cannot fire — the blip must be Candidate 1, on the first grounded packet after the sender reaches walkable ground. **The two shapes therefore have different blip producers**, which is precisely why the capture is worth taking. This is **NOT ESTABLISHED**. One further §1 consequence worth naming here: retail's `InterpolationManager::adjust_offset` **@0x00555d30** gates its entire body on `physics_obj->transient_state & 1` (CONTACT_TS) at 0x00555D52 — a remote in free fall gets no interpolation correction at all. acdream ports that gate (`InterpolationManager.cs:319-321`, `:346-349`) and the remote tick passes `inContact: rm.Body.InContact` (`RuntimeRemotePhysicsUpdater.cs:269`, `:306`) — but Link 1 forces `Contact` true unconditionally, so the gate never engages. Another correct port defeated by the same forged input. ### Do existing probes distinguish them? No. - `ACDREAM_PROBE_REMOTE_LANDING` (`PhysicsDiagnostics.cs:218-265`) fires only at the two landing *edges*. It logs `contact`, `onWalkable`, `gravitySet`, `resolveIsOnGround` — genuinely useful for confirming Links 3 and 4 at the landing instant, but it emits nothing during the slide window and nothing at the snap. It cannot see the wire bit or the classification. - `ACDREAM_PROBE_RESOLVE` (`PhysicsEngine.cs:2640`) logs `groundedIn / cp / hit / walkable` per resolve for every entity. It shows the contact-plane state but not the *result's* `OnWalkable`, not the plane's `Normal.Z`, and not the wire/classification side. It is also ~30 Hz × every entity — impractical for a two-client session. - `ACDREAM_REMOTE_VEL_DIAG` gives `[VEL_DIAG]` pace but no contact or classification data. ### The probe that would settle it One new per-accepted-Position line for remote GUIDs, emitted at the routing site in `LiveEntityNetworkUpdateController` (both arms), plus one per-tick line for the same GUID: **`[remote-slide-up]`** — guid, `update.IsGrounded` (the raw wire bit), `earlyRemoteRoute?.Disposition`, `request.PlayerDistance`, `bodyToTarget`, `willBeDrTicked`, `firstUp`, the `ApplyInterpolate` `Action` result (`Snapped`/`Enqueued`), `rm.Airborne`, `Body.TransientState` Contact/OnWalkable, `Body.State & Gravity`, `Body.Velocity`, `Body.ContactPlaneValid`, `Body.ContactPlane.Normal.Z`, wire `worldPos`, `Body.Position`, plus `Interp` queue depth and `_failCount` (needed to tell Candidate 1 from Candidate 2). **`[remote-slide-tick]`** — guid, `resolveResult.InContact`, `resolveResult.OnWalkable`, `resolveResult.IsOnGround`, `Body.ContactPlane.Normal.Z`, `Body.Velocity`, `Body.Acceleration`, the pre/post-integrate positions, and whether the body actually moved this tick. Rate-limit to remotes whose `ContactPlaneValid && ContactPlane.Normal.Z < PhysicsGlobals.FloorZ` (i.e. only while standing on something steep) so the volume stays usable in a live two-client run. That single capture answers: which shape, which snap site, and whether the contact plane the sweep finds on the roof is actually steep. --- ## 3. What retail does — verified directly against the pseudo-C All citations independently read out of `docs/research/named-retail/acclient_2013_pseudo_c.txt` for this report. ### 3.1 Retail's observer runs full physics for *every* object — no player fork `CPhysics::UseTime` **@0x00509950** (pseudo-C :271481) iterates a `LongHashIter` over the whole physics-object hash and calls `update_object` on each entry (pseudo-C :271639-271650): ``` 005099e0 class HashBaseData* curPtr_ = iter->curPtr_; 005099e5 CPhysicsObj::update_object(curPtr_); 005099ed if (curPtr_ == this->player) 005099f2 SmartBox::PlayerPhysicsUpdatedCallback(this->smartbox); 005099fa HashBaseIter::Next(this->iter); ``` The `curPtr_ == this->player` test only fires an extra callback. There is **no local-vs-remote fork on the update itself**. `CPhysicsObj::update_object` **@0x00515d10** (pseudo-C :283950-284055) has exactly one early-return gate (:283957): ``` 00515d40 if ((this_3->parent != 0 || (this_3->cell == 0 || (this_3->state & 0x1000000) != 0))) 00515eeb this_3->transient_state &= 0xffffff7f; // clear ACTIVE 00515ef5 return; ``` `0x01000000` is `FROZEN_PS` (ACE `PhysicsState.Frozen = 0x01000000`, `references/ACE/Source/ACE.Entity/Enum/PhysicsState.cs:32`). **There is no `is_player`, `autonomous`, `server_controlled`, or `MOVEMENT_LOCKED` gate.** `0x01000000` is `FROZEN_PS` (retail header `acclient.h:2841`, `enum PhysicsState`; cross-checked against ACE `references/ACE/Source/ACE.Entity/Enum/PhysicsState.cs:32`). The rest is the object clock: `player_distance` is computed (:283963-283976, this is retail's own `player_distance` field, the one `MoveOrTeleport` later reads); `set_active(1)` when within 96 m or when the object has no part array (:283983-283988); then dt gates — `< 0.0002` return, `> 2.0` discard, then the MaxQuantum subdivision loop into `UpdateObjectInternal` (:284035-284053). The 96 m test is a **distance LOD that applies to the local player identically**, not an identity fork. The only autonomy-flavoured field on `CPhysicsObj` is `last_move_was_autonomous`, written at 0x00514FE9 in `set_description` from `PhysicsDesc::get_autonomous_movement` and read by `CPhysicsObj::movement_is_autonomous` **@0x0050eb30** — whose only four callers are in `CMotionInterp::apply_raw_movement`/`apply_current_movement` (@0x0052888D, @0x005288ED, @0x0052894D, @0x00528991), i.e. deciding whether to *send* movement upstream. **It is never read by the physics tick.** `UpdateObjectInternal` **@0x005156B0** (pseudo-C :283611) and `UpdatePositionInternal` **@0x00512C30** (:280817) likewise contain no identity fork. `UpdatePositionInternal`'s only state-dependent branch of interest is `transient_state & 2` (ON_WALKABLE_TS, :280836), which decides whether the animation root-frame delta is scaled by `m_scale` or by zero — the branch acdream mirrors at `RuntimeRemotePhysicsUpdater.cs:133-135`, except that acdream keys it on the client `rm.Airborne` bool instead of the transient. **Answer: a retail observer runs the complete transition/slide for a remote, identically to the local player.** ### 3.2 `MoveOrTeleport` returning 0 does not suspend that simulation `CPhysicsObj::MoveOrTeleport` **@0x00516330** (pseudo-C :284304-284366) — verified verbatim: - `0x0051638E` — `if (arg4 != 0)` guards the entire near/far branch. - `0x0051636D` — `return 0` when it does not. Nothing is written. - `0x005163AF` — `InterpolateTo(this_1, arg2, IsMovingTo(this_1))` then `return 1` @0x005163BE, on `player_distance < 96f`. - `0x00516386` — cell-0/teleport branch, `teleport_hook` + flags-`0x1012` `SetPosition`, `return 1` @0x00516438. Nothing on the `return 0` path touches `update_time`, `FROZEN_PS`, `parent`, or `cell` — the three things `update_object` gates on. **The object keeps being ticked by `CPhysics::UseTime` at full rate.** This is the single most important retail fact for Bug B: retail's airborne no-op is safe *because* retail is simulating the object locally. acdream copied the no-op without the simulation. The caller confirms the same: `SmartBox::HandleReceivedPosition` **@0x00453FD0**, remote branch (pseudo-C :92995), is `if (MoveOrTeleport(...) != 0) { …ConstrainTo @0x00454272 }` and then returns. On the zero return it has done nothing but `unset_parent` @0x00454129 and the `!HasAnims`-gated `SetPlacementFrame` @0x00454142 — neither of which touches position, velocity, `FROZEN_PS`, or `ACTIVE_TS`. Incidental but worth recording: **`MoveOrTeleport` never references `arg5`**, the wire velocity vector. Retail discards the broadcast velocity for remotes entirely and relies on its own simulation — which is exactly the load acdream's `RuntimeRemotePhysicsUpdater.cs:169` cannot carry. ### 3.3 Retail derives `on_walkable` from the contact plane `CPhysicsObj::SetPositionInternal` **@0x00515330**, pseudo-C :283481-283509 (read out directly): ``` 00515430 if (arg2->collision_info.contact_plane_valid == 0) 00515437 eax_7 = (transient_state_1 & 0xfffffffe); // clear CONTACT_TS (0x1) 00515430 else 00515432 eax_7 = (transient_state_1 | 1); // set CONTACT_TS 0051543c this->transient_state = eax_7; 00515442 CPhysicsObj::calc_acceleration(this); ... 00515465 if ((eax_9 & 1) == 0) // not in contact 0051549f this->transient_state = (eax_9 & 0xfffffffd); // clear ON_WALKABLE (0x2) 005154a5 if ((eax_9 & 2) != 0) MovementManager::LeaveGround(...) 00515465 else 00515467 long double x87_r7_1 = this->contact_plane.N.z; 0051546d long double temp1_1 = PhysicsGlobals::floor_z; 00515478 if (contact_plane.N.z < floor_z) 0051548e CPhysicsObj::set_on_walkable(this, 0); 00515478 else 00515482 CPhysicsObj::set_on_walkable(this, 1); ``` Contact and on-walkable are **two independent facts**. A steep roof is Contact and not on-walkable. acdream ports this correctly in `PhysicsObjUpdate.ApplySetPositionContact` (`:30-56`) and `IsWalkableContact` (`:20-21`) — it simply never calls them on the visible remote path (Link 4). ### 3.4 What actually makes retail slide on a steep contact Three pieces, all present and correct in acdream's Core — none of them reachable by a grounded remote: 1. **Gravity survives.** `CPhysicsObj::calc_acceleration` **@0x00510950** (pseudo-C :278533; acdream's port is `PhysicsBody.cs:503-518`) zeroes acceleration only when `CONTACT && ON_WALKABLE && !SLEDDING` (0x0051096B). On a steep contact `ON_WALKABLE` is clear, so it falls through to the `state & GRAVITY_PS (0x400)` test at 0x005109F4 → `acceleration = (0, 0, PhysicsGlobals::gravity = -9.80000019)`. **`GRAVITY_PS` is on in the `CPhysicsObj` constructor** — `state = 0x400C08` at 0x00512508 (EDGE_SLIDE | LIGHTING_ON | GRAVITY | REPORT_COLLISIONS) — and is thereafter set wholesale from the wire by `set_description`'s `set_state(this, desc->state, 1)` @0x00514F40. `set_state` @0x00514DD0 post-processes only lighting / nodraw / hidden; **it never masks GRAVITY**. Retail never toggles the bit on a landing. 2. **Friction does not engage.** `CPhysicsObj::calc_friction` **@0x0050ee70** (pseudo-C :276694) opens with `if ((this->transient_state & 2) != 0)` at 0x0050EE7D — the *entire* function body, including the `v -= (v·N)N` into-plane removal and the `pow(1-friction, dt)` damping, is inside that `if`. On a non-walkable contact it returns immediately: no friction, and the into-surface component is never projected out. acdream's port has the identical gate (`PhysicsBody.cs:693-696`). 3. **The sweep projects the motion along the plane** — `CTransition` / `SlideSphere` @0x00537440 and the `SetPositionInternal` sliding-normal tail at 0x005154c2-0x005154e8. `UpdatePhysicsInternal` then integrates: `Velocity += Acceleration * dt` unconditionally (acdream `PhysicsBody.cs:771`). That is the slide. ### 3.5 Retail's interpolation is a per-frame *offset*, not the motion `PositionManager::adjust_offset` **@0x00555190** (pseudo-C :352090-352115) chains `InterpolationManager::adjust_offset` → `StickyManager::adjust_offset` → `ConstraintManager::adjust_offset` into the same `Frame` that `UpdatePositionInternal` then sends through the transition sweep. In retail the interpolation nudges an object that is *already* moving under its own physics. In acdream the interpolation catch-up is the object's *entire* motion (`RemoteMotionCombiner.ComposeOffset`, `:40-76`), because Links 1-3 removed everything else. --- ## 4. Is the roof even walkable? — the classification agrees; the consumer does not **acdream's classifier agrees with retail.** `PhysicsObjUpdate.IsWalkableContact` (`:20-21`) is `inContact && contactNormal.Z >= PhysicsGlobals.FloorZ` — the same predicate as retail's `contact_plane.N.z < floor_z` test above, and the same as retail's standalone `CPhysicsObj::is_valid_walkable` **@0x0050f530** (pseudo-C :277180), which is literally `return N.z < floor_z ? 0 : 1`. **Constant check — with a caveat.** acdream `TransitionTypes.cs:1255` carries `FloorZ = 0.6642f`, while `src/AcDream.Core/Rendering/Wb/TerrainUtils.cs:20` carries `0.66417414618662751f`. Two copies of the same constant in our own tree, differing by 2.6e-5 (about 0.002° of slope). Not load-bearing for Bug B, but it is an unexplained internal divergence and should be unified on the longer value. **The literal 0.66417414 does NOT appear anywhere in the pseudo-C** — grep returns nothing. `PhysicsGlobals::floor_z` lives at 0x008EDE5C and is computed at static-init in `$E73` **@0x0070d920** (pseudo-C :805181), which Binary Ninja renders as `floor_z = __fcos(3437.7467707849391)`. That rendering is a BN artifact of the classic elided-numerator kind (`3437.74677` is arcminutes-per-radian, so the real source is `cos( / 3437.7467…)`; BN dropped the numerator, exactly the class of defect `claude-memory/feedback_bn_decomp_field_names.md` warns about). The *value* is corroborated by WorldBuilder and ACE agreeing on 0.66417414618662751, but a byte decode of 0x0070D920 would be needed to pin it from the binary — see NOT ESTABLISHED #6. So the "acdream thinks it's walkable, retail doesn't" hypothesis is **half true, and its cause is not the threshold**. acdream's *sweep* would classify the roof exactly as retail does. The forged `OnWalkable` at `RuntimeRemotePhysicsUpdater.cs:150-154` overwrites that answer before the sweep ever runs, and `Tick` discards the answer the sweep returns anyway (Link 4). This is a plumbing defect, not a geometry or threshold defect. **NOT ESTABLISHED:** whether the specific house roof in the user's test actually produces a contact plane with `Normal.Z < 0.6642` in acdream's collision data. Buildings are GfxObj/Setup collision, and no capture of the contact plane on that roof exists. The `[remote-slide-tick]` probe's `ContactPlane.Normal.Z` field settles it. --- ## 5. Is it in the register / ISSUES already? **Yes, partially — three rows and one issue, none of them complete.** | Record | What it covers | Gap | |---|---|---| | `docs/ISSUES.md` **#32** (2026-08-04 addendum, ~:9562) | Bug B named, root-caused to the *landing block's* unconditional `OnWalkable`; cites `SetPositionInternal` @0x00515330 :283501-283509 correctly | Does not name `RuntimeRemotePhysicsUpdater.cs:150-154` (the dominant per-tick force), does not name `:169` (velocity zeroing), and treats Link 3 (Gravity) as a Bug A concern only. The stated fix target is therefore insufficient. | | register **AP-87** (`:242`) | The 4 m snap; its risk column *already predicted this exact symptom* and was updated 2026-08-04 with the live observation | Correct and current. AP-87 is the blip's mechanism, not its cause — do not "fix" AP-87. | | register **AP-81** (`:235`) | Remote VectorUpdate's non-retail Airborne/Gravity dance; risk column literally says "grounded remotes carry a non-retail state word" | Its file:line column lists only the VectorUpdate handler and the landing blocks. It does **not** list `RuntimeRemotePhysicsUpdater.cs:150-154`/`:169` — the per-tick unconditional force and velocity zeroing have **no register row of their own** and are not covered by AP-81's cited sites. Register rule 1 violation; the row needs extending in whichever commit next touches that file. | | register **AD-10** (`:122`) | Remote slope projection is terrain-normal-only (`RemoteMotionCombiner.ComposeOffset :65-73`, `ComputeOffset :163-168`), cannot see building/EnvCell geometry; already amended 2026-08-04 to name Bug B | Correct. Confirms that even a corrected `OnWalkable` gets no help from this path on a house roof. | **Verified `#32` is NOT a route-4a regression, independently:** the landing block's `TransientState |= Contact | OnWalkable` and the per-tick force at `:150-154` both predate C4. The per-tick force carries the comment "Forces OnWalkable + Contact so the gate in `apply_current_movement` always succeeds" (`:138-140`) — a #184-Slice-2b-era construct, unrelated to route 4a. Do not revert `44830a0e` or `7f1c1f5a`. **DO-NOT-RETRY check** (`claude-memory/project_physics_collision_digest.md`): the proposal in §6 does not appear in any DO-NOT-RETRY table. Closest neighbours, all distinct: the AD-25 landing `Velocity.Z = 0` hand-zero (:145-151) — this proposal *removes* a velocity zeroing rather than adding one, and does not touch the landing reflect; "do not seed transition contact from a caller's `isOnGround` bool when a body is present" (:149-150) — this proposal moves the remote *toward* body-derived contact, which is the same direction; #269's slope-slide residual (:152-160) — a *local-player* decay-curve question whose code is byte-exonerated, unrelated to the remote plumbing here. --- ## 6. Proposed fix ### The one-line summary The remote tick already runs retail's sweep and the sweep already computes retail's answer. **Stop forging the inputs and start consuming the outputs** — i.e. make the visible remote path use the same `PhysicsObjUpdate.CommitSetPositionTransition` seam that `TickHidden` and every other body in the codebase already use. ### Concrete targets 1. **`src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:150-154`** — delete the unconditional `Contact | OnWalkable` force from the grounded branch. Keep `Active`. Retail's equivalent state is written only by `SetPositionInternal` from the contact plane (`@0x00515330`, 0x00515430 / 0x00515465-0x0051548e). 2. **`RuntimeRemotePhysicsUpdater.cs:420-477`** — replace the bare `HandleAllCollisions` call with `PhysicsObjUpdate.CommitSetPositionTransition (body, resolveResult.InContact, resolveResult.OnWalkable, …, previousContact, previousOnWalkable, rm.Movement.HitGround, rm.Motion.LeaveGround, isCurrent)` — byte-identical in shape to `TickHidden.cs:778-795`, which already does exactly this. `CommitSetPositionTransition` calls `HandleAllCollisions` itself (`PhysicsObjUpdate.cs:104-110`), so this is a substitution, not an addition. Retail order is `SetPositionInternal` contact/walkable → `HitGround`/`LeaveGround` → `handle_all_collisions` @0x005154FE. 3. **`RuntimeRemotePhysicsUpdater.cs:493-495`** — the landing test currently uses `resolveResult.IsOnGround`, which is `inContact || …` (`PhysicsEngine.cs:2660`) and therefore true on a steep roof. It must be `resolveResult.OnWalkable`. Retail's ground edge is `set_on_walkable`, not contact. 4. **`RuntimeRemotePhysicsUpdater.cs:169`** — the `Velocity = Zero` must become conditional on the body actually being on walkable ground, or be deleted in favour of letting `calc_friction` (which retail gates on `OnWalkable`, `PhysicsBody.cs:693-696`) do the decay. Deleting it outright is the retail-faithful shape; making it conditional is the smaller step. 5. **`RuntimeRemotePhysicsUpdater.cs:571-576` and `LiveEntityNetworkUpdateController.cs:2110-2116`** — stop clearing `PhysicsStateFlags.Gravity`. Retail keeps GRAVITY_PS for the object's whole life and gates gravity acceleration on the Contact transient inside `calc_acceleration` (already correct in `PhysicsBody.cs:505-517`). This retires the bulk of register row **AP-81** and is shared with **Bug A**'s H1. 6. **`LiveEntityNetworkUpdateController.cs:2023-2024`** — the landing block's force, the site ISSUES.md #32 names. Necessary but, alone, useless: `:150-154` re-asserts it on the next tick. 7. **Register bookkeeping, same commit.** `AP-81`'s file:line column must gain `RuntimeRemotePhysicsUpdater.cs:150-154` and `:169` (§5). If items 1-5 land, AP-81 is largely retired and the row should be deleted or narrowed in the same commit per the register's rule 1. `AP-87` stays — it is the backstop, not the bug. **Explicitly NOT in scope:** `src/AcDream.Core/Physics/InterpolationManager.cs`. Its stall blip (§2 Candidate 2) is a faithful port of retail `InterpolationManager::UseTime` @0x00555f20 and is behaving correctly on a frozen body. Touching it would be a symptom-site guard. ### Ordering and prerequisites Items 1-4 are one coherent change and must land together — any subset leaves either a forged input or a discarded output. Item 5 is separable and is shared with Bug A. Item 6 falls out of item 1 (both sites express the same wrong idea). Item 7 is bookkeeping and rides whichever commit lands 1-4. ### Blast radius - **Every remote — players and NPCs alike.** Slice 2b deliberately collapsed the player/NPC fork (`RuntimeRemotePhysicsUpdater.cs:110-117`); there is one path. A remote will now genuinely fall, slide, and be subject to friction. This is the intent, and it is what makes it risky: **the #184 invisible-but-solid monster lived in this neighbourhood.** AP-87's 4 m snap stays as the backstop and must not be weakened in the same change (AP-137 explicitly warns that weakening it turns the leftover arm into a silent-freeze path). - **NPC free-fall / knockback** — the route 4a acceptance criterion "push a monster off a ledge, confirm it falls and lands without hovering" (`2026-08-03-c4-route-4a-contract.md:174-183`) is directly exercised by items 3 and 5 and must be re-run. - **Local player** — untouched. `PlayerMovementController` constructs its body with `State = Gravity | ReportCollisions` permanently (`PlayerMovementController.cs:689`) and already commits contact from the sweep. No shared code changes. - **Projectiles** — untouched. `RuntimeProjectilePhysicsUpdater` has its own stepper and its own state handling. - **Hidden remotes** — untouched; `TickHidden` already does the right thing and is the template. - **Sticky melee (TS-44) and de-overlap (#184/AP-86)** — the shadow-follows- resolved sync at `:618-630` reads `rm.Body.Position` after the resolve; a body that now genuinely moves under gravity will re-flood its shadow more often. Perf note, not correctness, but worth measuring in a packed town. - **The interpolation contact gate goes live.** `InterpolationManager.AdjustOffset`'s `inContact` early-return (`:319-321`, `:346-349` — retail's @0x00555D52 CONTACT_TS gate) currently never engages because Link 1 forges the bit. After item 1 it will, and an out-of-contact remote will stop receiving interpolation corrections mid-fall — which is retail-correct, and which also means Candidate 2 can no longer accumulate fail counts against a falling body. Expect the observable timing of any residual blip to change. ### Does it need live cdb evidence rather than a code change? **The retail side does not** — §3 establishes retail's behaviour from the pseudo-C conclusively, with no ambiguity requiring a runtime trace. `CPhysics::UseTime` iterating every object, `update_object`'s three-condition gate, `MoveOrTeleport`'s bare `return 0`, and `SetPositionInternal`'s floor_z comparison are all read directly and are unambiguous. **The acdream side does**, for one thing only: **which of Shape A / Shape B is happening on the wire**, and whether the roof's contact plane in acdream's own collision data is actually steep (§4's NOT ESTABLISHED). That is an *acdream* capture — the `[remote-slide-up]` / `[remote-slide-tick]` probe of §2 — not a retail cdb attach. Retail cdb is not the right tool here and would not answer either question. The honest recommendation: **land the probe, take one two-client capture, then implement items 1-4 together.** Implementing before the capture is defensible given how complete the code-side chain is, but the capture costs one session and tells us whether the wire is *also* starving the path (Shape A), which changes whether the `NoPositionOperation` branch needs its own follow-up. --- ## Shared root with Bug A? **Partially — one of three links is shared; the rest are not.** Shared: **Link 3, the Gravity clear.** Bug A's H1 (`2026-08-04-remote-landing-investigation.md:55-88`) is that the Gravity bit is already clear when `HitGround()` runs, so `CMotionInterp::HitGround`'s `state & 0x400` gate silently no-ops and the Falling pose never exits. Bug B's Link 3 is that the same clear removes the gravity acceleration that would drive the slide. **Item 5 of the fix addresses both.** Also shared: the **misclassified landing edge** (item 3). If the remote "lands" on a steep roof because `IsOnGround` is contact-derived, it runs the whole landing block — `HitGround`, Gravity clear, pose transition — on a surface retail is still sliding it down. That is a plausible amplifier for Bug A's symptom *specifically in the roof scenario*, though Bug A also occurs on flat ground, where it must have another cause. Not shared: Links 1, 2, and 4 (the per-tick force, the velocity zeroing, and the uncommitted sweep result) are pure position/physics and have no animation consequence. Bug A's H2 (no `DefaultSink` bound) and H3 (scheduler-side) have no Bug B analogue. --- ## NOT ESTABLISHED 1. **Which shape (A or B) the wire produces during a retail sender's roof slide** — i.e. whether ACE emits `IsGrounded == false` for the whole slide. Requires the `[remote-slide-up]` capture. Depends on whether ACE's *server-side* physics classifies a house roof as non-walkable, which was not traced. 2. **Whether the specific roof produces a contact plane with `Normal.Z < 0.6642` in acdream's collision data.** Requires the `[remote-slide-tick]` capture's `ContactPlane.Normal.Z` field. 3. **Whether items 1-4 are sufficient** to reproduce retail's slide, or whether the `#173` remote collision-velocity reflect (shipped but its gate folded into the never-run Campaign P matrix scenario 8) and AD-10's terrain-only projection also need work. ISSUES.md #32 already flags both as open dependencies and that assessment is confirmed here. 4. **Whether ACE relays a `0xF74E` VectorUpdate at all during a slide.** If it does not, the sender's slide velocity never reaches acdream even in principle, and the local simulation is the only possible source — which strengthens the fix but was not verified against ACE's broadcast conditions. 5. **Whether ACE's server-side physics classifies a house roof as non-walkable.** ACE emits `IsGrounded` from its own `PhysicsObj.TransientState & OnWalkable` (`PositionPack.cs:72-73`), so Shape A requires ACE's server physics to agree with retail's `floor_z` test on building geometry. Not traced. 6. **`PhysicsGlobals::floor_z`'s exact value from the binary.** The literal is absent from the pseudo-C and its static-init is a BN-mangled `fcos` (§4). The value is corroborated by two independent references but not by a primary byte decode of 0x0070D920. 7. **The polarity of `MoveOrTeleport`'s `player_distance` vs `96.0f` compare** at 0x00516393 — BN emits `bool p = unimplemented {test ah, 0x5}` and its synthesized `fcom` operand ordering is inconsistent across this dump. Nothing in this diagnosis depends on it: both arms `return 1` and both leave the object in the simulation set, and the observer in the user's test is far inside 96 m either way.