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

19 KiB
Raw Blame History

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 clobberingCellGraph.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_collisionscheck_other_cells) Core
79fb6e7 doorway blue-holeCurrCell 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.
  • 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 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)

$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.