docs(#185): design — Approach A (keep mover grounded on the forward move)
Root cause confirmed (decomp cross-check wf_3c1120c4-a04 + live apparatus): the outdoor stairs are a continuous coplanar 38.7-degree ramp of stacked step-box objects; at a seam the grounded forward move loses contact_plane_valid, the step-down recovery can't reach the coplanar (at-level) continuation, and EdgeSlide/PrecipiceSlide fabricates a horizontal (0,1,0) sliding normal that absorbs the up-stairs motion (the #137/TS-4 family). Fabrication math, the SetSlidingNormal Z-zero, and the multi-object search are all verified faithful; the divergence is upstream (retail keeps contact_plane_valid, pc 273244). Approach A: restore retail's grounded forward-move retention so the fragile step-down recovery isn't needed at seams. Exact retention-loss line pinned by a dat-backed replay test (the #137 method), not guessed. Alternatives B/C recorded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4a067e34a9
commit
06c3ecd89d
1 changed files with 171 additions and 0 deletions
|
|
@ -0,0 +1,171 @@
|
||||||
|
# Design: #185 — local player jams half-way up outdoor stairs (fix)
|
||||||
|
|
||||||
|
**Date:** 2026-07-08 · **Status:** DESIGN (approved approach: A) — implementation not started
|
||||||
|
**Milestone:** M1.5 (indoor world feels right) · **Family:** #137 / TS-4 synthetic-sliding-normal at a walkable seam, stair edition
|
||||||
|
**Investigation handoff:** `docs/research/2026-07-08-185-outdoor-stairs-phantom-handoff.md`
|
||||||
|
**Decomp cross-check workflow:** `wf_3c1120c4-a04` (5 tracers + 3 adversarial verifiers, all high-confidence)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Symptom
|
||||||
|
|
||||||
|
Running the **LOCAL player** up the outside stairs of a house-on-stilts, you hit an invisible
|
||||||
|
block part-way up: you shuffle sideways but cannot advance. **A jump clears it** and you
|
||||||
|
continue up normally. Pre-existing; unrelated to #184. Jam at world ≈ (132, 77.9, 61.5),
|
||||||
|
landblock `0xf682`, cell `0xF682002C`.
|
||||||
|
|
||||||
|
## 2. Root cause — CONFIRMED (decomp cross-check + live apparatus)
|
||||||
|
|
||||||
|
### 2.1 The geometry (dumped this session)
|
||||||
|
|
||||||
|
The staircase is a **continuous, coplanar 38.7° ramp** built from stacked multi-part
|
||||||
|
**step-box objects** (`entityId` `0x00F68220`, `0x00F68221`, `0x00F68222`, …; each holds
|
||||||
|
3 step parts; gfxObj `0x01000AC5` = one step-box, `0x01000ACA` = a sibling variant). Every
|
||||||
|
step's tread (poly id 4, local plane `(-0.625,0,0.781)`, world `(0,-0.625,0.781)`) lies on
|
||||||
|
the **same infinite plane** — verified: adjacent treads share both normal AND plane-d, so
|
||||||
|
they are genuinely coplanar, forming one continuous ramp. The tread quads abut at **0.5 m
|
||||||
|
seams in world Y**, and those seams fall on **object boundaries** (the objects interleave;
|
||||||
|
e.g. `0xF68222` part-3 at Y≈76.49 abuts `0xF68221` part-1 at Y≈76.50). gfxobj dumps:
|
||||||
|
`…/scratchpad/gfxdump/0x01000AC5.gfxobj.json`, `0x01000ACA.gfxobj.json`.
|
||||||
|
|
||||||
|
### 2.2 The mechanism (live capture `185-recapture.jsonl` + `[step-walk]` / `[stepdown-decide]`)
|
||||||
|
|
||||||
|
Walking up, acdream re-grounds every frame via the **step-down-to-maintain-contact** recovery
|
||||||
|
(`DoStepDown`, `TransitionTypes.cs:1339`). This *succeeds* at most positions (`accept=True`
|
||||||
|
climbing to Y=77.914). At one seam it fails, and the fabrication chain runs:
|
||||||
|
|
||||||
|
1. The grounded forward move floats the foot **past the current tread's polygon** (kept
|
||||||
|
"grounded" on the tread plane via the `LastKnownContactPlane` proximity restore) to
|
||||||
|
Y=78.12, Z=61.65 — but the forward `TransitionalInsert` reaches the **step-down dispatch
|
||||||
|
with `ci.ContactPlaneValid = false`** (the seed from `body.ContactPlane` is lost mid-sweep;
|
||||||
|
pinning *why* is implementation step 1, §3). Note: `ValidateTransition`'s LKCP proximity
|
||||||
|
restore later re-validates it, so `bodyAfter.contactPlaneValid = true` in the capture even
|
||||||
|
though the mover is wedged — the loss is transient-but-decisive, at the dispatch, not at
|
||||||
|
frame end.
|
||||||
|
2. Because contact is invalid at the dispatch, the step-down branch fires. `DoStepDown` first
|
||||||
|
**drops the sphere the full `stepDownHeight`** (Z 61.65 → 60.90) then sweeps `find_walkable`
|
||||||
|
**downward** — but the continuation tread is **coplanar (at the foot's level), now *above*
|
||||||
|
the dropped sphere** → nothing found → `[stepdown-decide] accept=False` (340× in the run).
|
||||||
|
3. `DoStepDown` fails → `EdgeSlideAfterStepDownFailed` → **branch3** (`TransitionTypes.cs:1516`)
|
||||||
|
→ `PrecipiceSlide` → `FindCrossedEdge` returns `cross(tread_normal, top_edge)` =
|
||||||
|
**`(0,±0.78,±0.62)`** → `SlideSphere` sets it as `CollisionNormal`.
|
||||||
|
4. `ValidateTransition` (`:4635`) → `SetSlidingNormal` **zeroes Z** → **`(0,±1,0)`**.
|
||||||
|
5. Body persists it (`PhysicsEngine.cs:1136`); next frame **seeds** it (`:1020`); `AdjustOffset`
|
||||||
|
absorbs the +Y up-stairs offset → **sideways-only wedge**, pinned at Y=77.914. Self-sustaining
|
||||||
|
(seed→absorb→step-0-abort), with a fresh `(0,0.78,0.62)` re-fabricated every ~6 ticks. A jump's
|
||||||
|
+Z velocity survives the horizontal `(0,1,0)` plane, which is why a jump clears it.
|
||||||
|
|
||||||
|
Capture signatures (`185-recapture.jsonl`): pinned `result.position = (131.71, 77.914, 61.485)`;
|
||||||
|
`bodyAfter.slidingNormal = (0,1,0)`; `result.collisionNormal = (0,0.78,0.62)` on the re-fabrication
|
||||||
|
ticks; `contactPlane = walkablePlane = (0,-0.625,0.781)` (the real tread).
|
||||||
|
|
||||||
|
### 2.3 The retail divergence (named-retail decomp, high confidence)
|
||||||
|
|
||||||
|
Retail's grounded forward move **keeps `contact_plane_valid`** and hits the short-circuit
|
||||||
|
`if (this->collision_info.contact_plane_valid != 0) return 1;` (`transitional_insert`, pc 273244 /
|
||||||
|
`0x0050b818`) — it **never runs step-down at an interior ramp point**, so it never reaches
|
||||||
|
`edge_slide`/`precipice_slide`. acdream instead **loses grounding on the forward move** and leans
|
||||||
|
on the fragile per-frame step-down recovery, which cannot reach a **coplanar (at-level)**
|
||||||
|
continuation because step-down is strictly downward. The wedge is a *symptom* of over-relying on
|
||||||
|
step-down where retail stays grounded.
|
||||||
|
|
||||||
|
### 2.4 What is FAITHFUL — do NOT touch (adversarially verified)
|
||||||
|
|
||||||
|
- `PrecipiceSlide` math (`find_crossed_edge` → `slide_sphere`, conditional sign-negate to oppose
|
||||||
|
travel): **faithful** (retail `precipice_slide` pc 274316/274354; conditional negate pc 274347).
|
||||||
|
- `SetSlidingNormal` **zeroing Z**: **faithful** — retail `set_sliding_normal` (`0x0050a060`,
|
||||||
|
pc 272137) also sets `sliding_normal.z = 0f` then normalizes. Un-zeroing is a DEAD END and would
|
||||||
|
itself be a divergence; and it is geometrically moot (both `cp.N` and the fabricated normal have
|
||||||
|
x=0, so the crease is pure-X for both the zeroed `(0,1,0)` and the 3D `(0,0.78,0.62)` — the +Y
|
||||||
|
offset is annihilated identically).
|
||||||
|
- The whole-cell **multi-object search**: **faithful** — `FindObjCollisionsInCell` iterates the
|
||||||
|
full per-cell shadow list; both stair objects are tested (`[resolve-bldg]` confirms hits on
|
||||||
|
`0xF68220/21/22`). Not a scoping/registration gap.
|
||||||
|
|
||||||
|
## 3. The fix — Approach A (keep the mover grounded on the forward move)
|
||||||
|
|
||||||
|
**Invariant to restore:** a grounded mover walking a continuous walkable surface **retains
|
||||||
|
`contact_plane_valid` on the forward move** (retail pc 273244), so it does not depend on the
|
||||||
|
step-down recovery that fails at a coplanar seam.
|
||||||
|
|
||||||
|
**Scope (narrow):** the grounded forward-move path in `Transition.TransitionalInsert` / the
|
||||||
|
contact-plane retention it feeds (`TransitionTypes.cs` ~1030–1110 Slid-clears + the neg-poly/
|
||||||
|
step-down dispatch ~1339, and the `ValidateTransition` LKCP retention ~4645–4735). The change:
|
||||||
|
when the grounded foot sphere grazes the **coplanar walkable continuation** at an object seam,
|
||||||
|
retain / re-establish the contact plane from the tread it is resting on, rather than clearing it
|
||||||
|
(a spurious Slid) and dropping into the step-down recovery.
|
||||||
|
|
||||||
|
**Explicitly NOT changed:** the fabrication code, `SetSlidingNormal`, `PrecipiceSlide`,
|
||||||
|
`FindCrossedEdge`, the multi-object search — all verified faithful (§2.4).
|
||||||
|
|
||||||
|
**Pinning the exact line — the #137 method, not a guess:** implementation step 1 is a **dat-backed
|
||||||
|
replay test** reproducing this exact seam crossing (fixtures already captured — §6). The replay
|
||||||
|
reproduces the wedge headlessly; the forward-move contact-plane transitions are instrumented
|
||||||
|
*inside the test* (plus a short `ACDREAM_PROBE_CONTACT_PLANE` live pass ONLY if the replay can't
|
||||||
|
reach the exact frame) to identify the precise line where `contact_plane_valid` is lost. The fix
|
||||||
|
targets *that* line to match retail's retention. Same pattern as `Issue137CorridorSeamReplayTests`.
|
||||||
|
|
||||||
|
**Scope guard:** if pinning reveals the faithful fix is larger than a localized retention change,
|
||||||
|
STOP and return to the user before expanding scope (frozen-internals rule).
|
||||||
|
|
||||||
|
## 4. Alternatives considered
|
||||||
|
|
||||||
|
- **B — make step-down find the coplanar/at-level continuation.** Bends `DoStepDown`/`StepSphereDown`
|
||||||
|
away from retail's strictly-downward design; risks being a subtle divergence rather than a faithful
|
||||||
|
fix. Rejected as primary.
|
||||||
|
- **C — gate `EdgeSlide`/`PrecipiceSlide` to not fabricate when a live walkable continuation exists.**
|
||||||
|
Smallest blast radius, and retail's `edge_slide` does gate on `walkable != 0`, but it treats the
|
||||||
|
symptom site and masks the upstream grounding loss (weaker against no-workarounds). Kept as the
|
||||||
|
fallback if A proves too invasive.
|
||||||
|
|
||||||
|
## 5. Test strategy
|
||||||
|
|
||||||
|
- **Red→green pin:** a new dat-backed replay (`Issue185OutdoorStairsSeamReplayTests`, pattern of
|
||||||
|
`Issue137CorridorSeamReplayTests`) driving the captured trajectory through the failing seam:
|
||||||
|
pre-fix reproduces the wedge (no advance past Y≈77.9, `slidingNormal=(0,1,0)`); post-fix the
|
||||||
|
mover climbs past the seam with no fabricated sliding normal.
|
||||||
|
- **Regression net (the whole point — shared grounded-move path):**
|
||||||
|
- `Issue137*` suites (`Issue137CorridorSeamReplayTests`, `Issue137SlidingNormalLifecycleTests`,
|
||||||
|
`Issue137CorridorSeamInspectionTests`) green / kept un-skipped.
|
||||||
|
- Live spot checks (user visual gate): indoor cellar/corridor stairs, other outdoor buildings,
|
||||||
|
ramps, and flat ground — **no new phantom blocks AND no players sliding through walkable
|
||||||
|
edges/ramps.**
|
||||||
|
- `dotnet build` + `dotnet test` green (Core / App / UI / Net).
|
||||||
|
|
||||||
|
## 6. Apparatus / fixtures (already captured this session)
|
||||||
|
|
||||||
|
- Structured resolve capture: `…/scratchpad/185-recapture.jsonl` (jam pinned at
|
||||||
|
`(131.71, 77.914, 61.485)`; `slidingNormal (0,1,0)`; `collisionNormal (0,0.78,0.62)`).
|
||||||
|
- gfxobj geometry: `…/scratchpad/gfxdump/0x01000AC5.gfxobj.json` (+ `0x01000ACA`) — the step-box
|
||||||
|
tread (poly 4) + faces; local tread plane `(-0.625,0,0.781, d=-0.312)`.
|
||||||
|
- `[resolve-bldg]` per-object world origins + hit-poly world verts (raw task `.output`); tread N
|
||||||
|
world verts `(132.75,77.495,61.015)…`; per-step `entOrigin_lb` e.g. `0xF6822103 → (132.0,77.2,60.4)`.
|
||||||
|
- Reusable probes: `ACDREAM_CAPTURE_RESOLVE`, `ACDREAM_PROBE_STEP_WALK`, `ACDREAM_PROBE_INDOOR_BSP`
|
||||||
|
(`[stepdown-decide]`), `ACDREAM_PROBE_BUILDING` (`[resolve-bldg]`, heavy — raw `.output`, not the
|
||||||
|
Tee'd log), `ACDREAM_PROBE_CONTACT_PLANE` (forward-move contact transitions — for site pinning),
|
||||||
|
`ACDREAM_DUMP_GFXOBJS` (fixture extraction).
|
||||||
|
|
||||||
|
## 7. Bookkeeping (in the fix commit)
|
||||||
|
|
||||||
|
- Register: amend **TS-4** (or add a new row) to record the grounding-retention change; retire/narrow
|
||||||
|
as the faithful port dictates.
|
||||||
|
- `claude-memory/project_physics_collision_digest.md`: add the #185 banner + DO-NOT-RETRY entries.
|
||||||
|
- `docs/ISSUES.md`: move #185 to Recently closed with the fix SHA.
|
||||||
|
- Roadmap/milestones: note the M1.5 #137-family stair fix.
|
||||||
|
|
||||||
|
## 8. Acceptance
|
||||||
|
|
||||||
|
- The LOCAL player **walks up the full outdoor staircase with no jam and no jump**; no sideways-slide
|
||||||
|
pin at the tread seams.
|
||||||
|
- Regression pass (§5): no new phantom blocks AND no players sliding through walkable ramps/edges;
|
||||||
|
`Issue137*` green; build + test green.
|
||||||
|
- Register bookkeeping done in the fix commit.
|
||||||
|
|
||||||
|
## 9. DO-NOT-RETRY (carried from the handoff + this investigation)
|
||||||
|
|
||||||
|
- Do NOT touch step-up budget / step-up logic — there is no riser; it is a continuous walkable ramp.
|
||||||
|
- Do NOT "fix the geometry" — the dat is real, coplanar, walkable treads.
|
||||||
|
- Do NOT un-zero Z in `SetSlidingNormal` — faithful to retail and geometrically moot (§2.4).
|
||||||
|
- Do NOT ship a per-frame sliding-normal clear or a small-offset-abort patch — fix the PROVENANCE
|
||||||
|
(the grounding loss), not the symptom (#137 lesson).
|
||||||
|
- Do NOT read the Tee'd launch log for `[resolve-bldg]` — it's buffered; read the raw task `.output`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue