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>
19 KiB
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):
- Membership pick ping-pong — the unordered
HashSet+ the pre-pick fork. Fixed by the verbatim ordered-CELLARRAYpick + retail's collide-then-pick order (Stage 1, faithful port). - Render-root clobbering —
CellGraph.CurrCell(the render root, "the player's cell") was written by the per-entityResolveWithTransition/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 theCurrCellwrite player-only (UpdatePlayerCurrCellviaUpdateCellId).
- Membership pick ping-pong — the unordered
- 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_listordered pick (CObjCell::find_cell_list @ 0x52b4e0, pc:308742): an orderedCellArray(= retailCELLARRAY), current cell at index 0 (add_cell@701036, pc:308766), single forward-walk expansion viafind_transit_cells(pc:308775-308785), then in-order, interior-wins-break pick (pc:308788-308825). Replaced the unorderedHashSet.- Collide-then-pick (
CEnvCell::find_env_collisions @ 0x52c130pc:309573 +CTransition::check_other_cells @ 0x50ae50pc:272717): the primary collision runs against the carried cell (the seed); the new cell is picked + all other cells collided after, in the new sharedTransition.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_transitioncurr_cell=check_cell, pc:272612); applied to the player's object-state cell viaUpdateCellId(=SetPositionInternal/change_cell, pc:283403/281192). - Pseudocode:
docs/research/2026-06-03-cell-membership-ordered-cellarray-pseudocode.md. - Plan:
docs/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 retailSmartBox::update_viewerspring-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#900x90stickiness, instead of retail's intrinsicfind_building_transit_cells(pc:318309). - #6 Outdoor
point_in_cell: the pick's outdoor fallback uses agx/gyXY-column adaptation (acdream landcells lack retail'sCLandCell::point_in_cellBSP). 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 orderedfind_cell_listpick inBuildCellSetAndPickContaining, the collide-then-pickRunCheckOtherCellsAndAdvance. Membership is stable (DELTA=0 while still; flap gone). - The blue-hole fix:
UpdatePlayerCurrCell(player-only render-root write). Do NOT re-add aCurrCellwrite insideResolveWithTransition/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
5ca2f44pre-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
DrawPortaloutdoor→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)
- This session: wrap (this handoff + doc/roadmap updates). DONE.
- 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.mdfirst; render is a different subsystem than this session's physics). - 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)
$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.