# AD-10 contract — remote slope projection at the combiner boundary **Date:** 2026-08-06 **Worktree:** `.claude/worktrees/peaceful-visvesvaraya-e0a196`, branch `claude/acdream-physics-divergence-5aa784`, HEAD `ef976c6d` **Register row:** AD-10 (`docs/architecture/retail-divergence-register.md:122`) **Status of this document:** planning contract. No production or test code was written; no commit was made. --- ## 0. Verdict up front **AD-10 cannot be asserted retirable today, and it must not be force-retired. But its retirement is *decidable*, and deciding it costs one offline measurement against a harness that already exists.** Three separate things were conflated in the row and are now separated: | | Claim | Status at HEAD | |---|---|---| | **A** | Retail's in-sweep contact-plane projection is missing from acdream | **False.** `Transition.AdjustOffset` (`src/AcDream.Core/Physics/TransitionTypes.cs:5180`) is a faithful port of `CTransition::adjust_offset` and runs per sub-step inside the sweep (`:1486`), and remotes *do* run that sweep (`src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:399`). | | **B** | The remote path has an *extra*, non-retail projection before the sweep | **True.** `RemoteMotionCombiner.ComposeOffset` `:65-73`, fed from `PhysicsEngine.SampleTerrainNormal` at `RuntimeRemotePhysicsUpdater.cs:278-282` and `:321-325`. | | **C** | That extra projection samples the wrong surface | **True and unambiguous.** `SampleTerrainNormal(x, y)` (`PhysicsEngine.cs:1019`) is a pure XY→landblock terrain lookup. It ignores the body's Z, its cell, buildings, EnvCells, statics, and other objects. | So AD-10 is **not a relocation of a missing mechanism**. It is an **additional pre-sweep projection layered on top of the faithful one**, against a surface retail never uses. That reframing gives two candidate ships, and the contract sequences them so the cheap measurement runs first: - **Stage 0 (measurement, no production change).** Determine whether the pre-sweep projection is redundant with the sweep's own `AdjustOffset`. If it is, **AD-10 retires by deletion** — the cleanest outcome available. - **Stage 1 (the fallback ship, if Stage 0 says the projection is load-bearing).** Change the *sample source*, not the mechanism: `SampleTerrainNormal(x, y)` → the body's own committed `ContactPlane.Normal`. That is 2 statements. It **retires AD-10's entire risk column** — the terrain/sweep disagreement, the cell-boundary sample, the props-underfoot sample, and the building/EnvCell blindness all vanish, because the projection surface becomes *the same plane the sweep uses*. What survives is only the row's first clause: the projection still happens at the combiner boundary rather than inside the sweep. AD-10 is **narrowed**, not retired. **Do not skip Stage 0 to get to Stage 1.** Stage 0 is the only thing that can justify a deletion, and deletion is strictly better than narrowing. **Do not skip Stage 1 to get to deletion.** The historical record (§2.4) shows the sweep was already present and already seeding a contact plane on the day the pre-sweep projection was added to fix a real, observed staircase. I could not establish from static reading why the sweep's projection was insufficient then. Assume it was for a reason until Stage 0 says otherwise. --- ## 1. Retail's mechanism, pinned ### 1.1 The function `CTransition::adjust_offset` @ **0x0050a370**, pseudo-C `docs/research/named-retail/acclient_2013_pseudo_c.txt:272271-272400`. > **Citation correction.** The register row cites `pc:272296-272346`. That range > lands *inside* the function but truncates both ends: it omits the > sliding-normal validity gate at the head and the entire post-projection > safety push-out block plus the sliding-normal-only tail at 272355-272400. > Cite **272271-272400** (the whole function) or the address **0x0050a370**. ### 1.2 What it does, verified by disassembly Binary Ninja's pseudo-C renders every x87 comparison here as the `fnstsw`/`test ah, imm8` mush. All four comparisons were re-read from the PDB-paired binary. Binary: `C:\Users\erikn\Downloads\acclient.exe`. `py tools/pdb-extract/check_exe_pdb.py` → `=== MATCH: this exe pairs with our acclient.pdb ===` (GUID `{9e847e2f-777c-4bd9-886c-22256bb87f32}`, age 1). Disassembled with capstone 5.0.7, image base 0x400000. ``` adjust_offset(this, out, offset): v = *offset keepSlide = 0 if (collision_info.sliding_normal_valid) { # 0050a3bc fcomp [0x795344] ; 0050a3c4 test ah,1 ; 0050a3c7 jne 0x50a3d1 # C0 alone == "less than" if (dot(v, sliding_normal) < 0) keepSlide = 1 else collision_info.sliding_normal_valid = 0 } if (collision_info.contact_plane_valid) { cAngle = dot(v, contact_plane.N) if (keepSlide) { cross = sliding_normal x contact_plane.N if (normalize_check_small(cross) == 0) v = cross * dot(cross, v) else v = 0 } else { # 0050a4fa fcomp [0x795344] ; 0050a502 test ah,0x41 ; 0050a505 jne 0x50a515 # 0x41 == C0|C3 == "less than or equal"; jne taken -> 0x50a515 (SUBTRACT) if (cAngle <= 0) v -= contact_plane.N * cAngle # 0x0050a515 else Plane::snap_to_plane(&contact_plane, &v) # 0x0050a50e } # post-projection safety push-out if (!contact_plane_is_water && contact_plane_cell_id != 0) { LandDefs::get_block_offset(&blockOff, sphere_path.check_pos.objcell_id, contact_plane_cell_id) dist = dot(global_sphere.center - blockOff, contact_plane.N) + contact_plane.d # 0050a5cf fcompp ; 0050a5d3 test ah,5 ; 0050a5d6 jp -> skip # test ah,5 / jp is the canonical "jump if >=" idiom if (dist < global_sphere.radius - 0.0002) { zDist = (global_sphere.radius - dist) / contact_plane.N.z # 0050a5eb fcompp ; 0050a5ed test ah,0x41 ; 0050a5f0 jne -> skip if (global_sphere.radius > |zDist|) SPHEREPATH::add_offset_to_check_pos(&sphere_path, (0, 0, zDist)) } } } else if (keepSlide) { v -= sliding_normal * dot(v, sliding_normal) } *out = v ``` Float constants read from the binary: `0x795344 = 0.0f` (bytes `00000000`); `0x7c6878 = 0.00019999999494757503f` (bytes `17b75139`). `Plane::snap_to_plane` @ **0x00509c50** (pc:271852): ``` if (|N.z| > 0.0002) # 00509c5f test ah,5 ; 00509c62 jnp v.z = -(v.x*N.x + v.y*N.y) / N.z # v.x, v.y UNTOUCHED ``` ### 1.3 The three answers the row asks for **What does retail project?** The **per-sub-step movement offset**, not the whole frame delta. `calc_num_steps` splits the transition into steps of one sphere radius; `adjust_offset` runs once per step (`find_transitional_position` @ 0x0050bdf0, call at pc:273695 / `0x0050bf66`). **Against what surface?** `collision_info.contact_plane` — *whatever surface the previous sub-step's collision actually found*. Its producers are: | Producer | Address | Surface class | |---|---|---| | `CTransition::init_contact_plane` | 0x0050e850 | initial seed | | `BSPTREE::step_sphere_down` | 0x0053a210 | **BSP polygons — buildings, EnvCells, dungeon geometry, statics** | | `BSPTREE::find_collisions` | 0x0053a440 | same | | `CSphere::step_sphere_down` | 0x00536d20 | **other objects** | | `CCylSphere::step_sphere_down` | 0x0053a9b0 | **other objects** | | `CSphere::intersects_sphere` / `CCylSphere::intersects_sphere` | 0x00537a80 / 0x0053b440 | other objects | | `CTransition::validate_transition` | 0x0050aa70 | last-known restore | Retail's projection surface is therefore the **general contact plane from any collidable geometry**. It sees buildings and EnvCells natively. This is the direct, primary-source confirmation of the gap AD-10's risk column names. **At what point in the sweep?** At the **head of each sub-step, before that step's `transitional_insert`**, consuming the plane the *previous* step established. acdream's port preserves that ordering verbatim (`TransitionTypes.cs:1483-1486` — `AdjustOffset` first, state cleared after, `:1523-1527`). ### 1.4 Two unregistered divergences found inside acdream's port of this function Both are in `Transition.AdjustOffset` (`src/AcDream.Core/Physics/TransitionTypes.cs:5180-5320`). Neither has a divergence-register row (grepped: no row mentions `snap_to_plane`, `SnapToPlane`, `naturalResting`, or `away-plane`). Both are **out of scope for this change** — but both are in the same function AD-10 points at, and one of them is directly about slope descent, so they are recorded here rather than absorbed. **(a) The `snap_to_plane` branch is substituted, not ported.** `TransitionTypes.cs:5252-5258`: ```csharp else { // Moving away from contact plane: snap to plane surface. result -= ci.ContactPlane.Normal * collisionAngle; branch = "away-plane"; } ``` This makes the `if` and the `else` **byte-identical** — both do `result -= N * collisionAngle`. Retail's `else` calls `snap_to_plane`, which adjusts **only Z** and leaves XY alone. For a slope of angle θ and a horizontal step of length `d`: | Direction | `dot(v,N)` | Retail result | acdream result | |---|---|---|---| | **Uphill** | `< 0` | `d·cosθ` along the plane (XY shrinks by cos θ) | identical | | **Downhill** | `> 0` | XY preserved at `d`, Z drops `d·tanθ` (speed along the plane `d/cosθ`) | XY shrinks to `d·cos²θ`, speed along the plane `d·cosθ` | So acdream descends slopes **slower than retail by a factor of cos²θ in XY**: 13% slow at 30°, 29% at 45°. Uphill is correct. This is a *plausible* contributor to the open **#269 slope-slide feel residual** (Campaign P), which CLAUDE.md records as still needing a live cdb A/B — worth handing to whoever picks #269 up, but **do not fold it into AD-10**: it changes local-player movement feel and needs its own visual gate. **(b) The safety push-out threshold is deliberately altered.** `TransitionTypes.cs:5285-5309` replaces retail's `radius` with `naturalRestingDist = radius * ContactPlane.Normal.Z` in both the trigger comparison and the `zDist` numerator. The code comment argues the case at length and says "ACE and the published pseudocode have the original threshold". The disassembly at 0x0050a5c4-0x0050a5ff confirms retail uses the bare `radius` in both places. Whether or not the correction is right, **an intentional deviation from a byte-confirmed retail constant with no register row is exactly what the register exists to catch.** **Action:** file both as register rows (or as one row with two clauses) in a separate commit. Neither blocks AD-10. --- ## 2. What remote bodies actually run at HEAD Established by symbol, not from inherited documentation. ### 2.1 The tick `RuntimeRemotePhysicsUpdater.Tick`, `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`: 1. `:142` — `bool bodyOnWalkableAtTickStart = rm.Body.OnWalkable;` 2. `:143-146` — root motion scaled by `objectScale` **only while OnWalkable** (retail `UpdatePositionInternal` @0x00512CA1). Zero otherwise. 3. `:278-282` / `:321-325` — **the AD-10 sample**: `bodyOnWalkableAtTickStart ? _physics.Engine.SampleTerrainNormal(Body.X, Body.Y) : null` 4. `:283-292` / `:326-335` — `rm.Position.ComposeOffset(..., terrainNormalNpc, inContact: rm.Body.InContact)` 5. `:293` / — `npcHost.PositionManager.AdjustOffset` (Sticky → Constraint) 6. `:301` / `:336` — `ApplyPositionManagerDelta` → `body.Position += Transform(delta.Origin, orientation)` (`:1087-1098`) 7. `:338-339` — `calc_acceleration()`, `UpdatePhysicsInternal(dt)` 8. `:371-451` — **`_physics.Engine.ResolveWithTransition(preIntegratePos, postIntegratePos, …, body: rm.Body, …)`** — the full sweep 9. `:453+` — commit position/cell, then `CommitSetPositionTransition` Steps 3-4 are the divergence. Step 8 is the retail mechanism. The `if (rm.Host is { } npcHost) / else` fork at `:265`/`:303` duplicates the sample and the `ComposeOffset` call **verbatim**. There are therefore **two identical production sites**, not one — any edit must touch both. (This is the AP-22 shape: a row naming one site where more exist.) ### 2.2 The sweep is real and the contact plane is real - `PhysicsEngine.ResolveWithTransition` (`:1888`) → `FindTransitionalPosition` (`:2094`) → the step loop (`TransitionTypes.cs:1472`) → `AdjustOffset` per step (`:1486`). - The result is **accumulated from the projected offsets** (`sp.AddOffsetToCheckPos(sp.GlobalOffset)`, `TransitionTypes.cs:1530`) — it is *not* clamped to `targetPos`. So the sweep genuinely produces a slope-following Z from a purely horizontal input offset. - The transition is **seeded** with the body's committed contact plane (`PhysicsEngine.cs:1984-1997`, register row IA-1), gated on retail's `check_contact` predicate (`dot(velocity, N) <= 0.0002`). Remotes pass `body: rm.Body`, so they get the seed. - The plane is **written back** to the body after every successful resolve (`PhysicsEngine.cs:2102-2145`), including `body.GroundNormal`. So `rm.Body.ContactPlane.Normal` at tick start is the real plane the previous tick's sweep found — terrain, BSP, or object. **This is the fact that makes Stage 1 a two-statement change.** The correct projection surface is already sitting on the body. ### 2.3 The third `ComposeOffset` call site is not part of AD-10 `TickHidden` at `:936-944` calls `ComposeOffset` with the `terrainNormal` parameter **omitted** (defaults to `null`). It never projects. Leave it alone; do not "make it consistent." ### 2.4 History — why the caution in §0 is not ceremonial - `9e4772a8` (**2026-05-05**) `fix(motion): project anim root motion onto terrain plane (slope staircase)` added the mechanism. The commit body documents a real, measured, user-visible ~5 Hz Z staircase and reasons it out correctly from the queue-empty fallback returning a Z=0 body-local delta. - `93cbabbc` (**2026-04-21**) `fix(physics): full retail per-frame chain for remote motion + persist ContactPlane across frames` — the sweep **and** the cross-frame contact-plane persistence were already present two weeks earlier. So the sweep's own `AdjustOffset` was live and plane-seeded on the day the staircase was observed, and it did not remove it. **I could not establish from static reading why.** Candidate explanations that have all landed *since*, any of which could have changed the answer: - `204d0ae0` (2026-08-04, Bug B / #32): removed a per-tick forge of `Contact | OnWalkable` and a per-tick `Body.Velocity = Vector3.Zero`, and made the tick run the full `CommitSetPositionTransition` sequence instead of consuming only `Position/CellId/IsOnGround`. - `2d611b2b` (2026-07-30, #265): replaced the `isOnGround`-driven contact seed with retail's `check_contact` predicate. - The 2026-07-30 `body.GroundNormal = ci.ContactPlane.Normal` sync (`PhysicsEngine.cs:2116-2127`), whose own comment says *"nothing wrote it from a live resolve before now — `calc_friction` always saw the `Vector3.UnitZ` default, i.e. every slope behaved like flat ground."* That last one is a documented instance of a slope-related value being silently flat for months. It is precisely the reason Stage 0 must be a **measurement**, not an argument. --- ## 3. Relationship to #32 (Bug B roof-plant) Read `docs/ISSUES.md:10267-10480` in full. **Fixing AD-10 does not fix #32, and does not partially fix it. The two are now disjoint.** Evidence: 1. #32's remote half is **already closed** — fixed `204d0ae0`, user-passed 2026-08-04 ("it lands and slides correctly now"). 2. The actual root cause was four forced writes in the remote tick, none of them AD-10's. The issue text states it directly: *"acdream's classifier was correct and was being overruled."* 3. The AD-10 projection is now gated on `bodyOnWalkableAtTickStart` (`:278`, `:321`). A steep roof produces `OnWalkable == false` (contact-plane `Normal.Z` 0.6097 against `FloorZ` 0.6642, measured live in the #32 capture), so **on the exact geometry #32 is about, the AD-10 path does not run at all.** **Therefore the AD-10 row's own risk column is now stale on this point.** It reads: *"a remote landing on a house roof gets no slope response from this path **regardless of `OnWalkable`**."* After Bug B's fix the clause is inverted — the path is gated *off* by `OnWalkable`, and the slide comes from gravity plus the sweep, exactly as retail does it. The same staleness is in #32's own AD-10 paragraph. **What AD-10 *does* still break is the case #32 never covered: a remote on a WALKABLE surface that is not terrain.** A flat or gentle roof, a bridge, a dock, a dungeon floor, a ramp inside a building. There `OnWalkable == true`, the gate opens, and `SampleTerrainNormal` returns the plane of the *ground far below* — an unrelated surface. That is worse than no projection: it applies a wrong plane rather than none. **This, not the roof-plant, is the live symptom AD-10 should be judged on.** **Do not promise #32 anything.** Its remaining open items (LeaveGround chatter bound, the `!Ok` airborne latch, `contact_allows_move`, and local-player edge-slide) are untouched by this work. --- ## 4. The exact change ### Stage 0 — measurement (no production change; may be discarded) Build the fixture in §7.1 and answer one question: **with the pre-sweep projection disabled, does the sweep alone track the surface Z?** - **If yes** → delete the projection: remove the `terrainNormal` parameter's two production feeds (`RuntimeRemotePhysicsUpdater.cs:278-282`, `:321-325`), the projection block in `RemoteMotionCombiner.ComposeOffset:65-73`, and the now-dead `terrainNormal` parameter. **AD-10 retires**; delete row 122. - **If no** → record the measured failure mode in the closeout doc (it is a real finding about the remote sweep either way) and ship Stage 1. ### Stage 1 — narrowing (the fallback ship) **Symbols touched — the complete list.** | File | Site | Change | |---|---|---| | `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs` | `:278-282` (host branch) | `SampleTerrainNormal(Body.X, Body.Y)` → `rm.Body.ContactPlaneValid ? rm.Body.ContactPlane.Normal : (Vector3?)null` | | `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs` | `:321-325` (no-host branch) | identical | | `src/AcDream.Core/Physics/RemoteMotionCombiner.cs` | `:48`, `:65-73`, `:91-104` | rename the parameter (`terrainNormal` → `contactPlaneNormal`) and rewrite the doc comment, which currently asserts a terrain sample | | `docs/architecture/retail-divergence-register.md:122` | AD-10 | rewrite: risk column collapses to the boundary-placement clause; correct the three stale claims in §10 | **Keep unchanged, deliberately:** - The `bodyOnWalkableAtTickStart` gate. It is what keeps a *wall* normal out of the projection (`OnWalkable` ⟺ `N.z >= FloorZ`), and it is the Bug B fix. - The `Normal.Z > 0.01f` guard inside `ComposeOffset`. Redundant under the `OnWalkable` gate but harmless and defensive. - The `!interpolationOverwrote` guard. The projection must stay confined to the queue-empty/head-reached case; an interpolation catch-up is already a 3D vector toward a server-reported Z and must not be re-projected. - `TickHidden` (`:936`). - `RemoteMotionCombiner.ComputeOffset` — see §10.3. Do not modify it as part of this change; if it is to be deleted, that is its own commit. **Why this is strictly safer than it looks.** - On outdoor terrain the two sources agree by construction: the committed plane on flat/rolling ground *is* the terrain triangle plane (`PhysicsEngine.cs:2113` writes `ci.ContactPlane`, whose terrain producer is the same `SampleTerrainWalkable` triangle `SampleTerrainNormal` reads). The staircase-removing behaviour the mechanism exists for is preserved identically. - The one-tick lag (committed plane from the previous sweep vs. a current-XY sample) is **more** retail-faithful, not less: retail's `adjust_offset` reads the plane the *previous* sub-step established. - It removes a whole class of failure rather than trading one for another — there is no scenario where a single-point terrain sample is right and the body's own committed contact plane is wrong. --- ## 5. Blast radius — both hosts ### 5.1 The graphical host `AcDream.App.Physics.RemotePhysicsUpdater` (`src/AcDream.App/Physics/RemotePhysicsUpdater.cs:17`) is a **thin adapter**: it constructs a `RuntimeRemotePhysicsUpdater` at `:46` and forwards (`:220-249`), supplying DAT shape dimensions and presentation callbacks. It contains **no duplicated projection**. Per-frame drive is `AcDream.App.Rendering.LiveEntityAnimationScheduler:25`. ### 5.2 The headless host — the finding that inverts the C5b lesson **`AcDream.Headless` never runs this code path at all.** Evidence, exhaustive: ``` $ grep -rn "new RuntimeRemotePhysicsUpdater" --include=*.cs src/ tests/ src/AcDream.App/Physics/RemotePhysicsUpdater.cs:46 <- only production site tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs:2884,2938,3017 tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs:406 tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs:414 ``` `src/AcDream.Headless/` contains no reference to `RemoteMotion`, `RemotePhysicsUpdater`, or `OrdinaryPhysicsUpdater`. The class is `internal` to `AcDream.Runtime` and reaches production only through `InternalsVisibleTo` into `AcDream.App`. The C5b lesson was "a survey over one host's call graph missed `AcDream.Headless` entirely." Here the reasoning that would produce the same mistake runs the other way: `RemoteMotionCombiner` *is* in `AcDream.Core`, and `RuntimeRemotePhysicsUpdater` *is* in `AcDream.Runtime`, so the reflex conclusion "therefore headless runs it" is available and **wrong**. Assembly placement is not evidence of reachability; the instantiation census is. Consequences for this contract: 1. **No headless gate is required or meaningful for AD-10.** Do not design one; a passing headless run would be vacuous evidence. 2. Headless *does* publish terrain, cell surfaces, buildings and static objects into the engine (`HeadlessSessionWorldProjection.cs:430-467` → `LandblockPhysicsContentBuilder.PublishStaticCollision`), so if remote DR is ever given to headless, Stage 1's change works there unmodified — the committed contact plane is available on both hosts. Stage 0's terrain-only assumption would *not* have been. 3. **#330's relevance is narrow.** Headless registering no live-entity collision means a headless body could never receive a contact plane from `CSphere`/`CCylSphere::step_sphere_down` (standing on another creature). That is a #330 consequence, not an AD-10 one, and it is unreachable today because headless does not run remote DR at all. ### 5.3 Adjacent gap observed, not claimed as a defect Headless bots appear to have **no remote dead-reckoning whatsoever** — remote entities would move only at `UpdatePosition` cadence. Whether that matters depends on what headless bots are for, which this contract does not decide. **Recommendation: file it as an issue with the §5.2 evidence, tagged as adjacent to #330, and let the headless owner judge severity.** Do not fold it into AD-10. --- ## 6. Gate design This is remote-movement **feel**; the acceptance test is the user's eyes, and it batches into the next connected session. Two clients: acdream observing an acdream-driven `+Acdream`, or acdream observing a retail-driven character. **Absence of a symptom is not the criterion.** Each row below names a *positive* observable. | # | Observable | Positive criterion | Why it is here | |---|---|---|---| | **G1** | **The ~5 Hz Z staircase** — a remote running across open rolling terrain (Holtburg fields, a hillside) | The remote's feet track the ground **continuously**. Watch specifically *between* server updates, at ~200 ms spacing. Any stepped/ratcheting Z is an immediate fail. | This is the artifact the current mechanism exists to remove. **A regression here is worse than the divergence** and is the single thing that vetoes the change. | | **G2** | **Slope descent smoothness** — the remote running *downhill* on a moderate grade | The descent reads as a continuous glide with the feet planted; no floating above the surface, no periodic sink-and-pop. Compare uphill on the same slope — they should feel symmetric. | Downhill is the branch §1.4(a) shows acdream already handles differently from retail, and the branch the queue-empty fallback dominates. | | **G3** | **The surface the divergence is actually about** — a remote walking on a **walkable non-terrain surface**: a bridge, a dock, a gently-pitched roof, a raised platform, a ramp or sloped floor inside a dungeon or building | The remote's feet stay **on that surface** while it moves, including across its slope. It must not sink toward, or drift with, the terrain below. | This is the case §3 identifies as AD-10's live symptom. It is the reason the change exists. | | **G4** | **Roof / steep-face behaviour** — repeat #32's own scenario: a remote jumps onto a house roof | Unchanged from the 2026-08-04 user-passed behaviour: it lands and slides down. | Guards the closed half of #32. The `OnWalkable` gate should make this a literal no-op; confirm rather than assume. | | **G5** | **Flat ground** — a remote walking on level terrain, and a remote standing still | Unchanged. Nothing new appears — no drift, no jitter, no Z creep while stationary. | The projection is a no-op on flat ground by construction; a change here means something else moved. | **Sequencing:** run G1 before anything else. If G1 fails, stop and revert — nothing below it matters. **Instrumentation:** `ACDREAM_PROBE_RESOLVE=1` gives one `[resolve]` line per `ResolveWithTransition` with the contact-plane status and the responsible entity guid, which is enough to correlate a visual observation to a specific remote. `ACDREAM_PROBE_CELL=1` is low-volume and useful for G3. Both are runtime-toggleable from the DebugPanel under `ACDREAM_DEVTOOLS=1`. **Build:** Release. Debug FPS produces false-regression alarms (`feedback_debug_vs_release_perf`), and G1 is a cadence observation. --- ## 7. Proof obligations and test plan **Standing rule for every test below: it does not count until the named sabotage has been applied and observed to redden it.** This campaign has shipped five green tests that covered nothing; the AP-22 review disproved a coverage claim by sabotage. Assume no discrimination until demonstrated. **No source-text pins. No test may re-encode the constant under test** — in particular, no test may compute its expected Z by re-implementing `v -= N·dot(v,N)`. Expected values come from the *geometry* (the surface the body is standing on), so a wrong-plane projection produces a wrong answer rather than a self-consistent one. ### 7.0 The existing harness `tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs` already drives the **production** `RuntimeRemotePhysicsUpdater.Tick` over a synthetic landblock, with `Harness.OnRamp(gradient)` / `Harness.Airborne(gradient, height)` and a `Tick(count, dt)` loop (`:304-460`). `PhysicsEngine.AddLandblock` there accepts terrain, a `CellSurface[]` and a `PortalPlane[]`. **Build on this harness.** It is the only existing fixture that produces a *real geometric* contact plane for a remote rather than a stubbed one, and it already models the fixture-validation pattern (`SteepTerrainProducesANonWalkableContactPlane`, `:55-64`). ### 7.1 T1 — Stage 0's decisive measurement: does the sweep alone track Z? **Layer:** `AcDream.Runtime.Tests`, the §7.0 harness. **Fixture:** `Harness.OnRamp(WalkableGradient)` with a non-empty root-motion frame driving the body along the slope, interpolation queue empty (so the fallback path runs). **Body:** tick N frames with the AD-10 sample forced to `null`, and assert the body's Z stays within a tight band of the terrain surface Z at its own XY (`TerrainSurface.SampleZ`), monotonically, with no per-tick Z plateau longer than one tick. **Sabotage that must redden it:** flatten the ramp gradient to 0 and invert the assertion — the test must not pass on flat ground for the wrong reason. And: short-circuit `Transition.AdjustOffset` to `return offset;` — T1 must go red, proving it is measuring the sweep's projection and nothing else. This test *is* Stage 0. Its result selects the ship. ### 7.2 T2 — the discriminating test: wrong plane vs. right plane **This is the load-bearing one.** It must fail on HEAD and pass after Stage 1. **Requirement (functional, not prescriptive):** a fixture in which the body rests on a **walkable surface whose plane differs from the terrain plane at the same XY**, with the difference established by the *sweep* (i.e. `Body.ContactPlane.Normal != SampleTerrainNormal(x, y)` at tick start). Two candidate constructions, in preference order: - **(a) Terrain crest.** Extend `Harness.Ramp` to a two-gradient heightmap (a ridge). Walk the body across the crest. At the tick after the crossing, the current-XY sample and the committed plane are from *different triangles*. Cheap, certainly buildable with the existing harness, and it exercises the row's "cell boundaries" risk directly. **Mandatory.** - **(b) Off-terrain walkable surface.** A sloped static/building collision surface above flat terrain, so the terrain sample is `(0,0,1)` while the committed plane is the platform. This is the case that matters most (§3) but needs a collision payload the current harness does not build — note that `CellSurface` is **not** consulted by `TransitionTypes`/`BSPQuery` (the former `HasCellSurface` path was deleted in C5a, `PhysicsEngine.cs:1806`), so it must go through the flat-collision/static publication path, not `AddLandblock`'s `cells` argument. **Required, with a documented fallback:** if the fixture cannot be built in reasonable time, say so explicitly in the closeout, ship on (a), and record (b) as an untested axis rather than silently dropping it. **Assertion:** the body's Z tracks the surface it is *standing on*, derived from that surface's own geometry — never from a re-implementation of the projection formula. **Fixture validation, mandatory and first:** before any motion assertion, assert `Body.ContactPlane.Normal` is the expected surface normal **and** that `SampleTerrainNormal` at the same XY differs from it. A fixture where the two agree cannot discriminate, and a green test on such a fixture is worthless. **Sabotage that must redden it:** revert the sample source to `SampleTerrainNormal`. T2 must go red. If it does not, the fixture does not discriminate and the test is void. ### 7.3 T3 — the production entry point is covered at all There is currently **no test of the production mechanism** (§10.6). Assert that `RemoteMotionCombiner.ComposeOffset`, on the `!interpolationOverwrote` path with a sloped normal, produces a body-local delta whose *world* rotation has the expected Z sign and magnitude; and that on the `interpolationOverwrote` path it produces **no** projection. **Sabotage:** invert the `!interpolationOverwrote` guard. T3 must go red. (Today, inverting that guard reddens nothing.) ### 7.4 T4 — the `OnWalkable` gate still closes on a steep face Reuse `Harness.OnRamp(SteepGradient)`. Assert no projection is applied while `Body.OnWalkable` is false, whichever normal source is wired. **Sabotage:** remove the `bodyOnWalkableAtTickStart` ternary at `:278`/`:321`. T4 must go red. This is the Bug B regression guard. ### 7.5 T5 — both fork branches `RuntimeRemotePhysicsUpdater` has **two** identical sites (`:278` host branch, `:321` no-host branch). At least one test must exercise the **no-host** (pre-`PositionManager`-binding) branch. **Sabotage:** apply the fix to only *one* branch. T5 must go red. This is the AP-22 lesson made a test: a row that names one site where several exist produces a fix that lands on one site. ### 7.6 Suite obligations - Full `AcDream.Runtime.Tests`, `AcDream.Core.Tests`, `AcDream.App.Tests` green. - Complete solution suite green. - No headless gate (§5.2) — and say so in the closeout, so its absence reads as a decision rather than an omission. --- ## 8. Verification hygiene — mandatory **This worktree's incremental build has twice served a stale DLL containing deleted code, including under `--no-incremental` and `-t:Rebuild`.** One reviewer had to delete all 44 `bin`/`obj` directories to get a truthful result. Any **verdict-deciding** test result — T1's Stage-0 answer, and every sabotage observation in §7 — must come from a genuinely clean build: ```powershell Get-ChildItem -Recurse -Directory -Include bin,obj | Remove-Item -Recurse -Force dotnet build -c Release ``` A sabotage that "did not redden the test" is **not** evidence until it has been re-run from a clean tree. The failure mode this guards against is exactly the one that makes a false coverage claim look verified. --- ## 9. Traps, size, split ### Traps 1. **Do not touch `Transition.AdjustOffset`.** Its two unregistered divergences (§1.4) are real, one of them plausibly feeds #269, and both change *local-player* movement feel. Fixing them inside an AD-10 commit puts a local-player regression behind a remote-movement gate. Separate commits, separate gates. 2. **Do not "unify" `TickHidden`.** It deliberately does not project (§2.3). 3. **Do not fix only one fork branch** (§7.5). 4. **Do not use `Body.GroundNormal` as the source.** It aliases `ContactPlane.Normal` on the success path but has a documented stale-retention branch (`PhysicsEngine.cs:2137-2145`). Read `ContactPlaneValid` + `ContactPlane.Normal` and treat invalid as `null`. 5. **Do not remove the `!interpolationOverwrote` guard.** Re-projecting an interpolation catch-up would fight the server's own Z. 6. **Do not promise #32 anything** (§3). 7. **Do not design a headless gate** (§5.2). 8. **Do not let the double projection go unexamined if Stage 1 ships.** After Stage 1, the offset is projected once at the boundary and again per-step inside the sweep, both against the same plane. That composition is *idempotent* (a vector already on the plane has `dot(v,N)==0`) — but say so explicitly in the closeout, with the arithmetic, rather than leaving it as an unexamined assumption. It is the reason Stage 0 exists. 9. **`SampleTerrainNormal` is XY-only and Z-blind.** It will happily return a normal for a body inside a dungeon, on a tower, or under a bridge, provided the landblock is resident. There is no "no terrain here" case to rely on. ### Size - **Stage 0:** ~1 test + ~40 lines of harness extension. Half a session, including the clean-build discipline. - **Stage 1 (if needed):** ~2 production statements, ~1 parameter rename, ~4 doc comments, 1 register-row rewrite, 4-5 tests. One session. - **The §1.4 register rows:** ~30 minutes, separate commit, no code. - **The §5.3 headless issue:** ~15 minutes, separate commit, no code. ### Split call **Do not split across agents.** Total production surface is a handful of statements; the expensive part is the fixture work in T2, which is a single coupled piece of reasoning about one harness. Splitting it would reproduce the "coupled plan slices given to parallel agents" failure (`feedback_dont_parallelize_coupled_plan_slices`). **Commit sequence:** 1. `test(physics): measure whether the remote sweep alone tracks surface Z (AD-10 Stage 0)` — T1 only, no production change. 2. Either `refactor(physics): delete the redundant pre-sweep slope projection (AD-10 retired)` **or** `fix(physics): project remote root motion onto the body's committed contact plane (AD-10 narrowed)` — with T2-T5 and the register edit **in the same commit** (same-commit row discipline). 3. `docs(register): file the AdjustOffset snap_to_plane and safety-threshold divergences` — §1.4, separate. 4. `docs: file the headless remote dead-reckoning gap` — §5.3, separate. Commits 1-2 are user-gated on §6 before anything downstream builds on them. --- ## 10. Claims found false or stale at HEAD Numbered, each with its evidence. **10.1 — AD-10's justification column is false.** The row says *"Remote bodies don't run a full local transition sweep."* They do: `RuntimeRemotePhysicsUpdater.cs:399` calls `_physics.Engine.ResolveWithTransition(...)` with the remote's own body, Setup-derived sphere list, step heights and mover flags, and `TickHidden:982` does the same. This is the premise the entire "cannot move it into the sweep" reasoning rests on, and it is the one that reframes the row from *relocation* to *addition*. **10.2 — The row describes its one live site backwards.** It cites *"`ComposeOffset` ~:65-72 **for the interpolation-active** boundary projection."* The guard at `RemoteMotionCombiner.cs:65` is `if (!interpolationOverwrote && ...)` — the projection runs **only when interpolation did *not* overwrite**, i.e. the queue-empty/head-reached case. The row has the two mutually exclusive cases swapped. **10.3 — The row's other cited site is dead code.** *"the queue-empty fallback ~:163-168"* is inside `RemoteMotionCombiner.ComputeOffset` (`:105-170`). `ComputeOffset` has **zero production callers** — `grep -rn "ComputeOffset" --include=*.cs src/` returns only its own definition (`:105`) and its internal call to `ComposeOffset` (`:143`, which passes `terrainNormal: null`). Its only callers are in `tests/AcDream.Core.Tests/Physics/`. So the row cites one live site described backwards and one correctly-described site that cannot execute in production. **10.4 — The row's risk column is stale on the roof clause.** It reads *"a remote landing on a house roof gets no slope response from this path **regardless of `OnWalkable`**."* Since Bug B (`204d0ae0`, 2026-08-04) the sample is gated on `bodyOnWalkableAtTickStart` (`RuntimeRemotePhysicsUpdater.cs:278`, `:321`), and a steep roof is `OnWalkable == false` by measurement (contact-plane `Normal.Z` 0.6097 vs. `FloorZ` 0.6642, from #32's live capture). The path is now gated *off* on exactly that geometry. The same stale clause is repeated verbatim in `docs/ISSUES.md` #32's AD-10 paragraph. **10.5 — The row's retail anchor range truncates the function.** `pc:272296-272346` omits the sliding-normal validity gate at the head (272276-272296) and the entire post-projection safety push-out block plus the sliding-normal-only tail (272355-272400). Both omitted regions are part of what "retail projects inside `adjust_offset`" means. Correct anchor: `CTransition::adjust_offset` **0x0050a370**, pc:272271-272400. **10.6 — The mechanism's only test tests a method production never calls.** `RemoteMotionCombinerTests.ComputeOffset_RootMotionFallback_SlopedTerrainNormal_ProjectsZOntoSlope` (`tests/AcDream.Core.Tests/Physics/RemoteMotionCombinerTests.cs:198-225`) is labelled *"Lock-the-fix for the 'remote running on a slope shows ~5 Hz Z staircase' bug"*. It exercises `ComputeOffset`, which is dead in production (10.3). It also hard-codes the expected result by re-implementing the projection formula in a comment and asserting the arithmetic — it cannot detect a wrong *plane*, only a wrong *multiply*. **The production path (`ComposeOffset` with a non-null normal) has no test at all.** This is the sixth green-test-covering-nothing in this campaign. **10.7 — Two divergences exist in acdream's `CTransition::adjust_offset` port with no register row.** (a) The `collisionAngle > 0` branch substitutes `v -= N·dot(v,N)` for retail's `Plane::snap_to_plane`, making the two arms of the `if/else` identical and shortening downhill XY travel by `cos²θ` relative to retail (`TransitionTypes.cs:5252-5258` vs. 0x0050a50e → 0x00509c50). (b) The safety push-out substitutes `radius * N.z` for retail's bare `radius` in both the trigger and the numerator (`TransitionTypes.cs:5285-5309` vs. 0x0050a5c4-0x0050a5ff), knowingly and with a written rationale, but with no row. Grep confirms: no register row mentions `snap_to_plane`, `SnapToPlane`, `naturalResting`, or `away-plane`. **10.8 — The reflex blast-radius inference is wrong here, in the opposite direction from C5b.** `RemoteMotionCombiner` is in `AcDream.Core` and `RuntimeRemotePhysicsUpdater` is in `AcDream.Runtime`, so "headless runs it too" is the available conclusion. It is false: the only production instantiation of `RuntimeRemotePhysicsUpdater` is `src/AcDream.App/Physics/RemotePhysicsUpdater.cs:46`, and `src/AcDream.Headless/` never names it. Assembly placement is not reachability. --- ## 11. What I could not establish Flagged rather than guessed. 1. **Why the sweep's own `AdjustOffset` did not remove the 2026-05-05 staircase**, given that both the sweep and cross-frame contact-plane persistence landed on 2026-04-21 (`93cbabbc`), two weeks earlier. Three subsequent changes (§2.4) could each have altered the answer. **This is exactly what Stage 0 measures**; do not proceed on either an assumption of redundancy or an assumption of necessity. 2. **Whether a body in a dungeon EnvCell gets a non-null `SampleTerrainNormal` in practice.** The function is XY-only and Z-blind, so it *will* return a normal whenever a landblock covering that XY is resident; whether pure dungeon landblocks stream terrain into `PhysicsEngine._landblocks` at all was not verified. This affects how bad the indoor case currently is, not whether the fix is correct. Determinable with `ACDREAM_PROBE_RESOLVE=1` in a dungeon. 3. **Whether §7.2(b)'s off-terrain fixture can be built against the current flat-collision publication API** in reasonable time. `CellSurface` is confirmed *not* to be the route (`TransitionTypes`/`BSPQuery` do not consult it; the `HasCellSurface` path was deleted in C5a). The static/building publication path was not traced end to end. §7.2 carries an explicit fallback for this. 4. **Whether §1.4(a) is a live contributor to #269.** The arithmetic is confirmed and the direction (downhill-only, XY-shortening) is suggestive, but #269's own note says the friction and jump chains were byte-verified identical and it needs a live cdb A/B. Handed over as a lead, not a diagnosis.