acdream/docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md
Erik a1b49f9b24 docs: wrap session — doorway flap FIXED (membership + blue-hole); A/B/C render residuals next
Canonical handoff: docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md
(what shipped: membership Stage 1 ordered-CELLARRAY port + the blue-hole render-root
clobbering fix; the full remaining-issues list — A camera-collision, B R1b particles,
C R2 outside-looking-in, Stage 2 membership, #7 stairs, the 5-test baseline; KEEP/
DON'T-REDO; key files + decomp anchors; copy-paste pickup prompt for next session).

- ISSUES.md: recorded the cottage doorway flap DONE (both causes) in Recently closed.
- render design spec §7: R1 + flap marked DONE; A/B/C mapped to the next render phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:09:57 +02:00

289 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handoff — membership Stage 1 + the doorway blue-hole are FIXED; render residuals A/B/C next — 2026-06-03
> **Canonical pickup for the next session.** Read this FIRST. This session **fixed the cottage
> doorway "flap"** — it had **two** independent causes (a membership pick bug AND a render-root
> clobbering bug), both now resolved and visual-verified by the user (inside-looking-out is correct).
> What remains are **three separate, already-known render phases** (A camera-collision, B particles,
> C outside-looking-in) and the **Stage 2** membership faithfulness rearchitecture. Branch:
> `claude/thirsty-goldberg-51bb9b`. PowerShell on Windows; launch logs are UTF-16
> (`Select-String` / ripgrep `--encoding utf-16-le`, NOT GNU grep).
---
## 0. TL;DR
- **FIXED (visual-verified):** the doorway flap. Inside-looking-out now renders correctly — solid
walls, wood floor, sky/terrain only through the doorway. No more full-screen bluish void.
- **The flap had TWO causes** (this is the key lesson — the prior handoff's "it's the pick" was only
half of it):
1. **Membership pick ping-pong** — the unordered `HashSet` + the pre-pick fork. Fixed by the
verbatim ordered-`CELLARRAY` pick + retail's collide-then-pick order (Stage 1, faithful port).
2. **Render-root clobbering**`CellGraph.CurrCell` (the render root, "the player's cell") was
written by the **per-entity** `ResolveWithTransition`/`ResolveCellId`, so a jumping NPC near the
doorway overwrote the player's render root every tick → the render rooted at the NPC's tiny
connector cell → only its ~8-triangle shell drew, rest = GL clear color = the blue void. Fixed
by making the `CurrCell` write **player-only** (`UpdatePlayerCurrCell` via `UpdateCellId`).
- **REMAINING (next sessions):** render residuals **A → C → B** (a fresh render session), then
membership **Stage 2** (uniform collision + intrinsic building entry), gated on a payoff (see §6).
- **Test state:** Core 1295 pass / 5 fail = the documented baseline (2 step-up + 3 door-collision),
**zero new breakage**. App 174 green.
---
## 1. Commit ledger (this session, branch `claude/thirsty-goldberg-51bb9b`)
| SHA | What | Layer |
|---|---|---|
| `b44dd14` | `CellArray` — ordered/deduped cell collection (retail `CELLARRAY::add_cell` @701036) | Core |
| `bc56545` | widen cell-candidate helpers `HashSet<uint>``ICollection<uint>` (non-behavioral) | Core |
| `22a184c` | **verbatim ordered-CELLARRAY membership pick** (`find_cell_list` @308742); deletes the `5ca2f44` pre-check | Core |
| `e5457f9` | **collide-then-pick** — remove the pre-pick fork; `RunCheckOtherCellsAndAdvance` post-step (`find_env_collisions``check_other_cells`) | Core |
| `79fb6e7` | **doorway blue-hole**`CurrCell` render-root write made player-only (`UpdatePlayerCurrCell`) | Core + App |
(`5ca2f44`, the prior session's current-first pre-check, was **deleted** by `22a184c` as the handoff directed.)
Working tree clean except untracked launch logs + the prior session's screenshots.
---
## 2. What shipped — Stage 1 membership port (faithful)
Ported verbatim from the decomp (`docs/research/named-retail/acclient_2013_pseudo_c.txt`):
- **`find_cell_list` ordered pick** (`CObjCell::find_cell_list @ 0x52b4e0`, pc:308742): an ordered
`CellArray` (= retail `CELLARRAY`), current cell at **index 0** (`add_cell` @701036, pc:308766),
single forward-walk expansion via `find_transit_cells` (pc:308775-308785), then **in-order,
interior-wins-break** pick (pc:308788-308825). Replaced the unordered `HashSet`.
- **Collide-then-pick** (`CEnvCell::find_env_collisions @ 0x52c130` pc:309573 +
`CTransition::check_other_cells @ 0x50ae50` pc:272717): the primary collision runs against the
**carried** cell (the seed); the new cell is picked + all other cells collided **after**, in the
new shared `Transition.RunCheckOtherCellsAndAdvance`. Removed acdream's pre-pick (line ~1958) which
retail does NOT have — that pre-pick was the engine of the position oscillation (it swapped the
collision geometry with the cell mid-tick → a bistable feedback loop).
- **Persistence** carried via the seed; committed by `ValidateTransition` (= `validate_transition`
`curr_cell=check_cell`, pc:272612); applied to the player's object-state cell via `UpdateCellId`
(= `SetPositionInternal`/`change_cell`, pc:283403/281192).
- Pseudocode: [`docs/research/2026-06-03-cell-membership-ordered-cellarray-pseudocode.md`](2026-06-03-cell-membership-ordered-cellarray-pseudocode.md).
- Plan: [`docs/superpowers/plans/2026-06-03-membership-ordered-cellarray-port.md`](../superpowers/plans/2026-06-03-membership-ordered-cellarray-port.md).
**Verified:** `[cell-transit]` dropped 47 → 13 → DELTA=0 while standing still (no oscillation); the
deterministic membership net is green (`CellTransit|FindEnvCollisions|CellGraph|Doorway|ResolveCellId`).
## 2b. What shipped — the doorway blue-hole fix (the real flap)
The ordered-pick fix alone did NOT clear the visual flap ("exactly the same"). Diagnosed via the
render probes (`ACDREAM_PROBE_FLAP`/`_VIS`/`_SHELL` + `ACDREAM_PROBE_CELL`):
```
[cell-transit] teleport -> 0xA9B40171 (player spawned in the ROOM, then stood still — no more transitions)
[flap-cam] root=0xA9B40170 ×77,951 frames (render rooted at the VESTIBULE — the wrong cell)
[shell] filter=1 [0xA9B40170:gfx=1 idx=24] (only 0170's ~8-tri shell drew → rest = blue clear color)
```
The player's cell was stable at `0171` (the room), but the render rooted at `0170` (a tiny connector)
because a Holtburg NPC (`0x000F4240`) jump-looping near the doorway clobbered `CellGraph.CurrCell`
every tick. `CurrCell` is documented as *"the player's cell"* (CellGraph.cs:19) and roots the indoor
render (GameWindow.cs:7172), but it was written by the **per-entity** `SetCurrAndReturn` inside
`ResolveWithTransition` (+ 4 sites in `ResolveCellId`).
**Fix (`79fb6e7`):** `CurrCell` is now written **only by the player** — new
`PhysicsEngine.UpdatePlayerCurrCell(cellId)` called from `PlayerMovementController.UpdateCellId`
(the single player chokepoint for CellId: teleport / server snap @ `SetPosition` + per-frame
resolver). Removed the write from `SetCurrAndReturn` (inlined the 2 resolve call sites) and the 4
`ResolveCellId` sites. NPCs no longer touch the render root. `CellGraphMembershipTests` rewritten to
the new contract (3 tests: `UpdatePlayerCurrCell` writes the root; `ResolveCellId` does NOT — the
blue-hole guard; stale-beats-null preserved).
**Verified:** `[flap-cam]` now shows `root=` the player's cell every frame (0171 in the room, 0170 in
the vestibule), `terrain=Planes/Skip` consistent, never stuck at an NPC's cell. User confirmed
inside-looking-out is correct.
> **Durable lesson (memory written):** a *single-owner* field (the player's render root) written from
> a *per-entity* loop is a clobbering bug. Membership/physics state that "belongs to the player" must
> be written at the player-only chokepoint, never in the shared per-entity resolve.
---
## 3. REMAINING ISSUES (the full list)
### Render residuals — visual, observed this session (do these next, a fresh render session)
- **A — interior walls go grey/transparent *while inside*; particles/NPCs visible through them.**
Cause: the 3rd-person chase eye sits OUTSIDE the player's cell (`[flap-cam] eyeInRoot=n`), so near
walls back-face/clip away. Fix: **camera collision** — port retail `SmartBox::update_viewer`
spring-arm to keep the eye in the cell. Partially present (`[flap-sweep]` runs: `desiredBack`/
`eyeBack`/`pulledIn`) but not fully containing the eye. Handoff-flagged "highest-leverage."
**Shares its root mechanism (eye-outside-cell) with C — do A first to shrink C.**
- **B — particles bleed through the ground/floor** (looking out from inside). → **R1b / issue #104**
(scene particles aren't cell-clipped; needs a cell link on the `ParticleEmitter`). Deferred in the
R1 plan. Smallest of the three.
- **C — outside-looking-in: interior walls transparent + ground texture over the floor** (the
cottage renders as a see-through box from the street; includes a detached floating wall piece). →
**R2 / `PView::DrawPortal @ 0x5a5ab0`** — the outdoor→interior portal render, not yet built. The
biggest (a whole render phase).
**Recommended order: A → C → B** (A first = highest-leverage + shrinks C; C second = the big one,
fresh; B last = small, self-contained).
### Membership Stage 2 — faithfulness debt, NOT a visible bug (do after A/B/C; gate on payoff — §6)
- **#4 Forked collision (§4.4 #3):** still branches `cellLow >= 0x0100` (indoor cell-BSP vs outdoor
terrain-triangles) + `TryFindIndoorWalkablePlane`. Retail does one uniform sphere-sweep over all
cells. "A real rearchitecture" (handoff's words) — risky; the membership WORKS without it.
- **#5 Building entry is a bridge (§4.4 #4):** `CellTransit.CheckBuildingTransit` + the `#90` `0x90`
stickiness, instead of retail's intrinsic `find_building_transit_cells` (pc:318309).
- **#6 Outdoor `point_in_cell`:** the pick's outdoor fallback uses a `gx/gy` XY-column adaptation
(acdream landcells lack retail's `CLandCell::point_in_cell` BSP). Documented adaptation.
### Separate physics — UNVERIFIED this session
- **#7 Stairs Z-oscillation** (~0.2 m/tick on the cellar stairs, #98 family). The prior handoff §8
said it MAY be mooted by the membership fix. **Not re-tested** — needs a cellar-stairs walk.
**This is the gate for Stage 2:** if the stairs are still broken, uniform collision (#4) plausibly
fixes it and Stage 2 earns its risk; if the stairs are fine, Stage 2 stays optional.
### Test baseline — NOT introduced this session (do not "fix" blindly)
- **5 Core failures:** `BSPStepUpTests.B1`, `BSPStepUpTests.D4` (2 step-up gaps) +
`DoorCollisionApparatusTests.Apparatus_Grounded_50cmOffCenter_FrontApproach_DocumentsBug` +
`DoorBugTrajectoryReplayTests.LiveCompare_DoorOffCenterWalkthrough_Tick13558` +
`…LiveCompare_DoorBlocksFromOutside_Tick22760` (3 door-collision). Verified pre-existing; the
with-change failure set is a strict match to the prior baseline.
---
## 4. KEEP / DON'T-REDO
**KEEP (correct, verified — do not reopen):**
- The Stage-1 membership port: `CellArray`, the ordered `find_cell_list` pick in
`BuildCellSetAndPickContaining`, the collide-then-pick `RunCheckOtherCellsAndAdvance`. Membership
is stable (DELTA=0 while still; flap gone).
- The blue-hole fix: `UpdatePlayerCurrCell` (player-only render-root write). Do NOT re-add a `CurrCell`
write inside `ResolveWithTransition`/`ResolveCellId` (that was the clobbering bug).
- The R1 render (per-cell `DrawInside`, binary decision) — inside-looking-out is correct.
**DON'T:**
- Don't re-add the `5ca2f44` pre-check (the ordered array subsumes it; the prior handoff's "ordered
pick is too shallow" framing was itself superseded — the pick WAS necessary, it just wasn't the
whole flap).
- Don't treat Stage 2 as mandatory — it's faithfulness debt; the membership works. Gate it on #7.
- Don't conflate A and C as one fix — A is camera-collision (eye containment while inside), C is the
separate `DrawPortal` outdoor→interior phase. They share a *mechanism* (eye/viewpoint vs cell), not
a code path.
---
## 5. KEY FILES + ANCHORS
```
MEMBERSHIP (Stage 1 — shipped)
src/AcDream.Core/Physics/CellArray.cs ← retail CELLARRAY (ordered+dedup)
src/AcDream.Core/Physics/CellTransit.cs
BuildCellSetAndPickContaining ← the ordered find_cell_list pick
src/AcDream.Core/Physics/TransitionTypes.cs
FindEnvCollisions (~1939) ← primary-collide-against-seed (no pre-pick)
RunCheckOtherCellsAndAdvance (~after FindEnvCollisions)← the check_other_cells post-step
src/AcDream.Core/Physics/PhysicsEngine.cs
UpdatePlayerCurrCell (~259) ← player-only render-root write (blue-hole fix)
ResolveWithTransition (~608, returns sp.CurCellId)
src/AcDream.App/Input/PlayerMovementController.cs
UpdateCellId (~774) ← calls _physics.UpdatePlayerCurrCell
tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs ← new contract (3 tests)
tests/AcDream.Core.Tests/Physics/CellTransitFindCellSetTests.cs ← ordered-pick conformance
RENDER (residuals A/B/C — next)
src/AcDream.App/Rendering/GameWindow.cs (OnRender ~7300-7600) ← binary decision, landscape-thru-door, Z-clear
src/AcDream.App/Rendering/InteriorRenderer.cs ← per-cell DrawInside loop
src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs ← [flap-sweep] camera-collision (A lives near here)
src/AcDream.App/Rendering/CellVisibility.cs ← ComputeVisibilityFromRoot (root=player cell)
src/AcDream.App/Rendering/ClipFrameAssembler.cs ← TerrainMode / HasOutsideView (consistent)
src/AcDream.App/Rendering/PortalVisibilityBuilder.cs ← the PView BFS ([flap] probe)
(R2/DrawPortal does not exist yet — C builds it)
RETAIL DECOMP (docs/research/named-retail/acclient_2013_pseudo_c.txt)
CObjCell::find_cell_list 0x52b4e0 pc:308742 (pick 308788-308825; add_cell @701036)
CEnvCell::find_env_collisions 0x52c130 pc:309573 (primary BSP vs the carried cell — no pre-pick)
CTransition::check_other_cells 0x50ae50 pc:272717 (build array + collide others + advance)
CTransition::validate_transition 0x50aa70 pc:272547 (curr_cell = check_cell @272612)
CPhysicsObj::SetPositionInternal 0x515330 pc:283399 (change_cell iff this->cell != curr_cell)
SmartBox::update_viewer (camera spring-arm — A) ; PView::DrawPortal 0x5a5ab0 (C)
PROBES
ACDREAM_PROBE_CELL=1 [cell-transit] (player CellId changes)
ACDREAM_PROBE_FLAP=1 [flap-cam] root/eyeInRoot/terrain/eye-vs-player + [flap] PView BFS + [flap-sweep] camera-collision
ACDREAM_PROBE_VIS=1 [vis] visible cells + OutsideView
ACDREAM_PROBE_SHELL=1 [shell] per-cell shell draw (NOSNAP/gfx/idx/zh/tr)
```
---
## 6. SEQUENCING DECISION (agreed with the user, 2026-06-03)
1. **This session: wrap** (this handoff + doc/roadmap updates). DONE.
2. **Next session: render residuals A → C → B** (a fresh render session — load the render design spec
`docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md` first; render is a different
subsystem than this session's physics).
3. **After A/B/C: membership Stage 2** (#4 uniform collision + #5 intrinsic entry), **gated on #7**
first re-test the cellar stairs; if still oscillating, Stage 2 (uniform collision) is the likely
fix and earns its risk; if fine, Stage 2 stays optional faithfulness debt.
---
## 7. RUNNING THE CLIENT (PowerShell; `+Acdream` spawns at/near the Holtburg cottage)
```powershell
$env:ACDREAM_DAT_DIR="$env:USERPROFILE\Documents\Asheron's Call"; $env:ACDREAM_LIVE="1"
$env:ACDREAM_TEST_HOST="127.0.0.1"; $env:ACDREAM_TEST_PORT="9000"
$env:ACDREAM_TEST_USER="testaccount"; $env:ACDREAM_TEST_PASS="testpassword"
$env:ACDREAM_PROBE_FLAP="1"; $env:ACDREAM_PROBE_CELL="1" # for A: watch eyeInRoot + the camera sweep
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug *>&1 | Tee-Object -FilePath launch.log
```
Build green BEFORE launching. Logs are UTF-16. Close gracefully (CloseMainWindow, not Stop-Process) so
ACE clears the session in ~3-5s.
---
## 8. PICKUP PROMPT (copy-paste for the next session)
```
RENDER RESIDUALS A → C → B at the Holtburg cottage doorway. The cottage doorway "flap" is FIXED this
session (membership Stage 1 + the blue-hole render-root clobbering) — inside-looking-out renders
correctly, user-verified. Continue on branch claude/thirsty-goldberg-51bb9b (do NOT branch/worktree;
do NOT push without asking; NEVER git stash/gc — a shared stash is under investigation). PowerShell on
Windows; launch logs are UTF-16 (Select-String / ripgrep --encoding utf-16-le, NOT GNU grep).
READ FIRST:
1. docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md (THIS handoff — what shipped,
the full remaining-issues list §3, KEEP/DON'T-REDO §4, key files §5, sequencing §6).
2. docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md (the render redesign — R1
shipped + correct; R1b/R2 are the remaining phases; §2 the binary model, §4 the seal mechanics).
THE JOB — three known render phases, in order (render is a DIFFERENT subsystem than the physics this
handoff describes — load the render design spec, work in GameWindow.OnRender / CellVisibility /
PhysicsCameraCollisionProbe / PortalVisibilityBuilder / ParticleRenderer):
- A (camera collision — highest-leverage, do FIRST): interior walls go grey/transparent WHILE INSIDE
because the 3rd-person chase EYE sits outside the player's cell ([flap-cam] eyeInRoot=n) → near
walls back-face/clip away. Port retail SmartBox::update_viewer spring-arm to keep the eye in the
cell. A [flap-sweep] camera-collision already runs (desiredBack/eyeBack/pulledIn) but doesn't fully
contain the eye — refine it. Shares the eye-outside-cell mechanism with C, so A shrinks C.
- C (R2 outside-looking-in, do SECOND — the big one): from the street the cottage renders as a
see-through box (interior walls transparent + ground texture over the floor). Build the
outdoor→interior portal render — retail PView::DrawPortal @ 0x5a5ab0 (ConstructView(CBldPortal) →
recurse → DrawCells the interior through the door's clip). Not built yet.
- B (R1b / #104, do LAST — smallest): particles bleed through the ground/floor when looking out.
Cell-clip scene particles (give the ParticleEmitter a cell link via ParentCellId; clip per-cell in
the DrawInside loop).
WORKFLOW: render is verified ON SCREEN (user's eyes) + the probes (ACDREAM_PROBE_FLAP/_VIS/_SHELL/
_CELL), never off the test suite (CLAUDE.md). For AC-specific render algorithms use the WB pipeline +
the retail decomp as the oracle (design spec). superpowers:writing-plans → executing-plans; verify
each phase at a user visual gate. Get a screenshot EARLY for render bugs (memory: render-one-gate).
DO NOT RE-LITIGATE: the flap is FIXED (membership + render-root, both committed + verified — KEEP §4).
Do NOT re-add a CurrCell write inside the per-entity ResolveWithTransition/ResolveCellId (that was the
blue-hole clobbering bug). Do NOT re-add the 5ca2f44 pre-check.
AFTER A/B/C: membership Stage 2 (uniform collision #4 + intrinsic building entry #5), but GATE it on
issue #7 first — re-test the cellar stairs (ACDREAM_PROBE_CELL + ACDREAM_CAPTURE_RESOLVE): if the
~0.2m/tick Z-oscillation persists, uniform collision is the likely fix and Stage 2 earns its risk; if
the stairs are fine, Stage 2 stays optional faithfulness debt (the membership works without it).
TEST BASELINE: Core 1295 pass / 5 fail (2 step-up BSPStepUp B1/D4 + 3 door-collision DoorCollision/
DoorBug) = pre-existing, NOT yours. App 174 green.
```
```