docs: A6.P3 #98 resolution + A6.P4 design + #99/#100 filed

Knowledge-preservation pass after the issue #98 cellar-up fix shipped
(`b3ce505`). Closes the saga's documentation loop and plans the next
phase.

Changes:
  - docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md
    Appended "Resolution 2026-05-24" section: v3 hypothesis falsified,
    actual mechanism (head-bump cottage GfxObj floor poly from below)
    confirmed, b3ce505 fix shipped, known door regression flagged.
    Memory artifacts cross-referenced.
  - docs/ISSUES.md
    #98 moved to DONE with full resolution writeup + decomp anchors.
    #99 filed: door regression at building thresholds (caused by
    b3ce505's indoor-primary gate). Closes via A6.P4.
    #100 filed: transparent rectangular patches around houses
    (terrain rendering). Bisect found commit 35b37df introduced the
    hiddenTerrainCells mechanism that collapses 24m outdoor cells
    when buildings sit in them; cottage building only fills part of
    its cell so the rest of the 24m cell shows the sky-bleeding gap.
    Three fix-path options documented.
  - docs/superpowers/specs/2026-05-24-phase-a6-p4-retail-shadow-architecture.md
    Full A6.P4 design doc. Three-slice plan: (1) query-side portal
    expansion to close #99 while preserving #98 fix, (2) port retail's
    BuildShadowCellSet at registration time so per-cell semantics match
    `CObjCell::find_cell_list`, (3) remove b3ce505 stopgap entirely.
    Decomp anchors, file-by-file plan, risk inventory, open questions.

Memory entries written separately (out-of-tree at
~/.claude/projects/.../memory/):
  - feedback_retail_per_cell_shadow_list.md
    The architectural lesson: retail uses per-cell shadow_object_list
    with portal-aware registration; our landblock-wide spatial
    registry diverges at indoor/outdoor seams.
  - feedback_apparatus_for_physics_bugs.md
    The apparatus-first pattern that cracked the saga: live capture +
    fixture dump + replay harness. Template for future physics bugs.
    Quote rule: "when a physics bug is resisting and you catch
    yourself about to ship 'fix attempt N+1 with no new evidence,'
    STOP. Build the apparatus first."
  - MEMORY.md index updated with both new entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-24 07:23:49 +02:00
parent b3ce505ca8
commit b55ae831bd
3 changed files with 487 additions and 1 deletions

View file

@ -534,3 +534,116 @@ LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered
test is now in documents-the-bug form (PASSES while bug exists; FAILS
when fix lands) — flip it when the cottage GfxObj is registered.
```
---
## Resolution 2026-05-24
### What was wrong with the evening-v3 hypothesis
The v3 "stale ramp contact plane" hypothesis (top of this doc) was
**FALSIFIED** by chronological walk of `a6-issue98-resolve-capture-2.jsonl`:
- Player position at the first cap event (tick 55101, line 55102 of the
JSONL): world `(141.605, 7.304, 92.656)`
- `bodyBefore.walkableVertices`: the ramp polygon at world
X∈[140.5, 142.1], Y∈[5.80, 8.70], Z∈[90.99, 93.99]
- Player XY is **inside** the ramp polygon's footprint
- `bodyBefore.contactPlane.normal` = (0, 0.7189884, 0.69502217) — the
ramp's plane
The v3 doc claimed "ramp at world X∈[129.7, 131.3], 10m away from
player." That geometry was computed from a wrong source (not the actual
ramp polygon). The live capture's `walkableVertices` are the ground
truth and show the player IS on the ramp at the cap event. The contact
plane is the ramp's plane because the player is on the ramp — correct,
not stale.
Tick 55020 (line 55021) shows the contact plane refreshing in real time
as the player crossed onto the ramp: `bodyBefore` had the previous
polygon's plane, `bodyAfter` had the ramp's plane. The walkable-refresh
chain works. No drift mechanism exists in the way v3 described.
### What the actual mechanism was
The evening-v2 finding was correct: head-sphere bumps the cottage
GfxObj's downward-facing floor poly (poly 0 in the GfxObj fixture, a
triangle covering world X∈[136.3, 142.5], Y∈[3.5, 19.5], Z=94) from
below. Player at (141.605, 7.304) is inside that triangle. Head sphere
top at Z=foot+1.68=94.336 penetrates the cottage floor at Z=94 by
0.336m → cn=(0,0,-1) push-back → stuck.
Why retail doesn't have this cap: decomp grep of
`CObjCell::find_obj_collisions` (line 308916) shows retail iterates
`this->shadow_object_list` — a **per-cell list**. `CObjCell::find_cell_list`
(line 308742) branches indoor/outdoor at registration time: indoor adds
only the indoor cell + portal-visible neighbors; outdoor adds all
overlapping outdoor cells via `add_all_outside_cells`. So a landblock-
baked static like the cottage gets added to outdoor cells'
shadow_object_list only — never to indoor EnvCells like the cellar.
`CEnvCell::find_collisions` therefore never tests the sphere against
the cottage when sphere is inside the cellar.
`sides_type` (the polygon flag the v2 finding option (b) speculated
about) does NOT affect retail's BSP collision code — it only appears in
rendering/mesh-batch code. The collision-path divergence is purely
architectural: per-cell list vs spatial-radius registry.
### What shipped (commit b3ce505)
Smallest behavioral patch matching retail's effect at the query level:
- `ShadowObjectRegistry.GetNearbyObjects` gained an optional
`primaryCellId` parameter. When indoor (≥ 0x0100), the outdoor radial
sweep is skipped — only indoor-scoped shadows from `indoorCellIds` are
returned.
- `Transition.FindObjCollisions` passes `sp.CheckCellId`.
- Harness `LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal`
flipped to `LiveCompare_FirstCap_FixClosesCottageFloorCap` — asserts
the downward-facing cottage-floor cap does NOT fire after the fix.
- Residual-X-motion test deleted — it documented post-cap edge-slide,
irrelevant once the cap is gone.
Verified: 11/11 cellar harness tests pass. 55 directly-affected physics
tests pass. Pre-existing static-state leakage failures (819 across
serial runs) unchanged. Full `dotnet build` clean.
Visual verification: user confirmed "Finally I can go up!" in the
Holtburg cottage cellar.
### Known regression caused by b3ce505 + next phase
Doorway edge case (flagged in the commit message): doors are server-
spawned entities with their own cylinder collision, registered via
`UpdatePosition` to whichever cell their position resolves to. Doors at
building thresholds typically resolve to outdoor cells. With the
indoor-primary radial-sweep gate, a sphere inside an indoor doorway-
adjacent cell doesn't see the outdoor door → can walk through.
User reported this: "I can also run through doors."
This regression is the direct consequence of NOT doing retail's full
portal-aware shadow propagation at registration time. Retail's
`find_cell_list` indoor branch recurses through `VisibleCellIds` and
adds the object to all portal-visible cells. Our `Register` doesn't do
this; the b3ce505 stopgap covers cottage-cellar but not doorways.
**Next phase: A6.P4 — port retail's per-cell shadow_object_list
architecture in full.** Design spec at
`docs/superpowers/specs/2026-05-24-phase-a6-p4-retail-shadow-architecture.md`
(this session). Approach: refactor `ShadowObjectRegistry.Register` to
compute the cell set via the retail-faithful indoor/outdoor branch +
portal-visible recursion (using `CellPhysics.VisibleCellIds`). Eliminate
the cellScope=0 spatial approximation. `GetNearbyObjects` becomes pure
per-cell list iteration. Removes the b3ce505 stopgap. Closes the door
regression as a side effect.
Also-likely-closed by A6.P4: #97 (phantom collisions on 2nd floor),
indoor sling-out (Finding 3 family), other indoor/outdoor seam bugs.
### Memory updates (this resolution)
- `feedback_retail_per_cell_shadow_list.md` — the architectural lesson
- `feedback_apparatus_for_physics_bugs.md` — the apparatus pattern that
finally cracked this saga (template for future physics bugs)