User's own decomp dig (verified): the flap's deepest root is architectural, not the
find_cell_list pick ordering. Retail membership is persistent object STATE (curr_cell
mutated ONLY by change_cell at a portal crossing); acdream RE-DERIVES CellId from
FindCellSet geometry every tick → ping-pong. Plus multi-valued CELLARRAY (retail) vs
single CellId (acdream), uniform vs forked collision (0x0100), intrinsic vs bridge
building entry. Reframed the handoff + prompt: the pick-ordering port (§4.3) is
SUPERSEDED/symptomatic; the job is STAGE 1 = persistent + multi-valued + portal-
crossing membership (change_cell 281192, find_transit_cells, SetPositionInternal),
drop the 5ca2f44 pre-check; STAGE 2 = uniform collision + intrinsic entry. New §4.4
(the 4-point analysis) + §4.5 (staged fix).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 KiB
Handoff — Port retail's persistent / multi-valued cell membership (the R1 "flap" fix) — 2026-06-02
Canonical pickup for the next (fresh) session. Read this FIRST, then the linked design docs. This session shipped R1 — the per-cell
DrawInsiderender redesign (the interior seal works: the cellar is solid), and that render redesign exposed a pre-existing cell-MEMBERSHIP ping-pong — the visual "flap" the user sees at every cottage threshold. The render is correct; the membership answer it consumes is unstable. The next job is to port retail's persistent, multi-valued, portal-crossing cell membership — in retail, membership is object state mutated only bychange_cellat a portal crossing; in acdream it's a per-tick geometric recomputation (§4.4). NOTE: an earlier framing in this doc (a verbatim port offind_cell_list's pick ordering) is SUPERSEDED by §4.4/§4.5 — it treated a symptom.
0. THE MANDATE + the new authorization (user, 2026-06-02 — non-negotiable)
- The standing render mandate is unchanged: fully working outdoor + indoor + dungeon rendering, no shortcuts/bandaids, port from retail, architecturally-correct even if slower. (See the master render handoff + design spec, §13 links.)
- NEW, EXPLICIT authorization (this is the key enabler): "You are allowed to break whatever you want in the code to get the engine and membership working, it's OK!" — i.e. do the faithful verbatim port even if it breaks other physics/movement tests or behavior. Don't tiptoe around the #98-area accumulated logic. Get the membership right, faithfully; fix any breakage afterward. Run the physics suite to SEE what breaks (eyes open), don't avoid the port.
- The user also clarified the fidelity split: physics/membership = strict, line-by-line faithful decomp port (this task); rendering = retail-structured orchestration over the kept WorldBuilder pipeline (already done in R1 — do NOT re-port rendering from decomp).
- The user is tired of probe-driven debug cycles — do NOT ask them to run manual probe walks. Diagnose from existing data + the existing apparatus (trajectory-replay harness, auto-logging probes); verify the fix with a normal visual test (their eyes) + the deterministic harness.
1. THE MENTAL MODEL (render vs membership) — internalize this first
Two separate systems, in a strict producer→consumer relationship:
- Membership (physics,
AcDream.Core.Physics): every tick, answers ONE question — "which cell (room/space) is the player standing in?" → a singlecurrentCellper tick. The world is carved into cells: cellar0174, stairs0175, main room0171(+ sub-cells0172/0173), vestibule0170, outdoors0031(note: low id0x31 < 0x100⇒ outdoor landcell). - Render (
AcDream.App.Rendering):draw(currentCell)— strictly downstream. IfcurrentCellis jittery, render faithfully redraws the jitter.
The flap = render correctly drawing an oscillating membership answer. Proof it's membership,
not render: when membership is stable (standing still in the cellar) the render seals
perfectly (solid walls/floor, no bleed — user confirmed). The flap appears only at the
boundaries where the [cell-transit] log shows the cell answer oscillating. Fix membership →
render goes stable; the seal already works.
Confidence note (honest): very high the flap is membership (the cellar-seals proof + the flap-tracks-cell-flips evidence). NOT a claimed 100% that zero render residual exists — a single clean outdoor→indoor transition has never been observed (the ping-pong masks it), so a small render tear on a genuine one-time crossing could exist. If so it's separate + isolated, and becomes visible once membership is stable. The membership fix is unambiguously the right next step regardless.
2. WHAT THIS SESSION SHIPPED (commit ledger, branch claude/thirsty-goldberg-51bb9b)
| SHA | What | Keep? |
|---|---|---|
7aca79f |
R0 — locked render-redesign design spec (brainstorm outcome) | ✅ authority |
ce7404b |
R1 implementation plan (per-cell DrawInside, TDD) | ✅ authority |
cf85ea4 |
R1 Task 1 — InteriorEntityPartition (3-bucket entity split, TDD, 3 tests) |
✅ correct |
4b75c68 |
R1 Task 2 — InteriorRenderer per-cell DrawInside loop |
✅ correct |
c4fd711 |
R1 Task 3 — binary render decision in GameWindow.OnRender (indoor = DrawInside only) |
✅ correct |
58822fe |
R1 Task 4 — repurpose the WbDrawDispatcher.cs:1756 ParentCellId==null bypass (#78) |
✅ correct |
5ca2f44 |
membership pre-check approximation (current-cell-first explicit check) | ⚠️ REPLACE (see §5) |
Working tree is clean at 5ca2f44 (untracked = session screenshots + launch logs only).
R1 (the render redesign) is done and proven correct by the visual gate (cellar seals). The
only open R1 blocker is the membership flap (this handoff).
Design + plan docs (read after this handoff):
- Design spec:
docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md - R1 plan:
docs/superpowers/plans/2026-06-02-render-r1-per-cell-drawinside.md - Master render handoff:
docs/research/2026-06-02-render-pipeline-redesign-handoff.md - Retail render reference:
docs/research/2026-06-02-retail-render-pipeline-full-reference.md
3. THE PROVEN DIAGNOSIS (the flap = membership ping-pong)
Captured from the user's R1 walk (ACDREAM_PROBE_CELL, [cell-transit] lines). 59 cell
transitions in one cottage walk (a clean walk should be ~6–8) — oscillating at every boundary:
- Stairs ↔ cellar (
0175 ↔ 0174) at ~(154.3, 9.4, 93.1). Crucially, the foot Z oscillates ~0.2 m/tick (93.07 ↔ 93.27; low Z = cellar, high Z = stairs). ⇒ membership is faithfully following a bouncing position — this looks like a SEPARATE stairs/ramp physics instability (step-up/step-down, #98 family), not (only) the pick. See §8 — do not conflate. - Room ↔ room (
0171 ↔ 0173 ↔ 0172) at constant Z = 94.0, tiny X/Y movement. ⇒ pure membership-pick non-determinism (the unordered HashSet). This is the verbatim-port target. - Vestibule ↔ outdoors (
0170 ↔ 0031) at ~(155, 16).0031is an outdoor landcell, so each flip swings the binary render decision to the outdoor path → the full sky/world/NPCs flash ("bluish background"). Fixed by the same current-first hysteresis (vestibule wins while it contains you).
The [vis] log confirms the mechanism: as the root cell flips, the OutsideView + visible-cell
set flip with it (e.g. room 0171 → outside(polys=0) sealed vs stairs 0175 →
outside(polys=1) sky-through-portal), so the through-door landscape appears/disappears = the flap.
Evidence files (this session, may still be present in the worktree root): launch-r1.log,
launch-fix.log (UTF-16 — read with Select-String / ripgrep --encoding utf-16-le, NOT GNU grep).
4. ROOT CAUSE + the verbatim retail target
4.1 acdream (the bug)
CellTransit.BuildCellSetAndPickContaining (src/AcDream.Core/Physics/CellTransit.cs, the body
of FindCellSet):
- The candidate set is an unordered
HashSet<uint>(declared ~line 433). - The current cell IS added first (~line 447, indoor seed) — but
HashSetdoes not preserve insertion order, and the set's contents churn tick-to-tick at a boundary, so the enumeration can surface a neighbour before the current cell. - The pick (the
foreach (uint candId in candidates)interior pass, ~lines 544–562 post-5ca2f44): iterates the HashSet in arbitrary order, returns the first interior cell whoseCellBSPcontains the sphere center (interior-wins). Outdoor fallback = agx/gyXY-column computation (NOT retail'spoint_in_cellon landcells — acdream landcells have nopoint_in_cell). - The player's
CellIdis set from this:ResolveWithTransition(PhysicsEngine.cs:608) returns the sweptsp.CurCellId(lines 880/901), whichFindEnvCollisions(TransitionTypes.cs:1958) sets fromFindCellSet;PlayerMovementController.cs:1296doesUpdateCellId(resolveResult.CellId, "resolver"). A1 (swept membership) IS ported — the divergence is purely the pick ordering.
4.2 retail (the verbatim source) — CObjCell::find_cell_list @ 0x52b4e0 (pc:308742–308831)
Reads verbatim this session. The structure:
// edi = arg4 = the CELLARRAY (an ORDERED DArray<CELLINFO{cell_id, cell*}>)
num_cells = 0; added_outside = 0;
objcell_id = arg1->objcell_id; // the CURRENT cell
cell0 = (objcell_id >= 0x100) ? CEnvCell::GetVisible(objcell_id) : CLandCell::GetVisible(objcell_id);
if (objcell_id >= 0x100) CELLARRAY::add_cell(edi, objcell_id, cell0); // CURRENT at INDEX 0 (pc:308766)
else CLandCell::add_all_outside_cells(arg1, arg2, arg3, edi);
// expand: for each cell already in the array, call its find_transit_cells (vtable[0x80]) (pc:308782)
for (i = 0; i < edi->num_cells; i++)
if (edi->cells[i].cell) edi->cells[i].cell->vtable[0x80](arg1, arg2, arg3, edi, arg6);
// THE PICK (pc:308788–308825): iterate the array IN ORDER from index 0, interior-wins-break
*arg5 = nullptr;
for (i = 0; i < edi->num_cells; i++) {
cell = edi->cells[i].cell;
if (cell) {
// point relative to the cell's block offset:
Vector3 p = arg3->center - LandDefs::get_block_offset(arg1->objcell_id, cell->cell_id);
if (cell->vtable[0x84](&p) != 0) { // point_in_cell (pc:308810)
*arg5 = cell; // set result on ANY containing cell
if ((int16_t)cell->cell_id >= 0x100) { // interior?
arg6->hits_interior_cell = 1;
break; // INTERIOR-WINS — stop
}
}
}
}
// (then do_not_load_cells prune, pc:308829+ — out of scope for the flap)
The load-bearing facts: (a) the current cell is at index 0; (b) the pick iterates in
order and breaks on the first interior-containing cell. So the current cell is tested
FIRST — if you're still inside it, it wins and the search stops. That ordered, current-first
iteration IS the hysteresis: you stay in your current cell until the center genuinely leaves it,
never flipping to an overlapping neighbour. acdream's unordered HashSet discarded that ordering.
CELLARRAY::add_celldefinition was not located by grep (void CELLARRAY::add_cell(→ no match; only call sites at pc:279012/288076/308766/309860/309960/310030/310054/310208/317064/317218). Behaviorally it appends to the ordered array; treat the candidate collection as ordered + deduped (the HashSet already dedups; an ordered-dedup collection is the faithful model). The fresh session can locate add_cell via Ghidra MCP (/decompile_function) oracclient.h(CELLARRAY =acclient.h:31574) if exact dedup semantics are wanted.
4.3 ⚠️ SUPERSEDED — the pick-ordering fix is TOO SHALLOW (see §4.4)
The "ordered-CELLARRAY pick" framing below treats a symptom, not the root. The user's 2026-06-02 architectural analysis (§4.4) showed the real divergence is membership is state in retail but a recomputation in acdream. A current-first / ordered pick only makes the per-tick re-derivation stickier (a better band-aid). Do §4.4 Stage 1 instead. This subsection is kept only to explain why the pick fix is insufficient.
Replace the unordered — insufficient: it reorders a per-tick recomputation; it does not make membership
persistent state mutated only at portal crossings.HashSet candidate set with an ordered, deduped collection (mirror
retail CELLARRAY: current cell at index 0, neighbours in BFS add-order, unique) and port the
pick verbatim: iterate from index 0; for each interior cell, point_in_cell via
BSPQuery.PointInsideCellBsp(cell.CellBSP.Root, localCenter); first interior-containing →
return (break).
4.4 THE DEEPER ROOT (user analysis, 2026-06-02) — membership is STATE, not recomputation
The user's own decomp dig (verified against the tree this session) reframes the whole task. Four architectural differences, root → consequences:
| Aspect | Retail | acdream | Verified |
|---|---|---|---|
| #1 Membership representation | Persistent curr_cell pointer (object STATE), acclient.h:32641 |
CellId recomputed per tick from foot-sphere geometry |
PlayerMovementController.cs:1296 ← resolveResult.CellId ← FindCellSet pick (TransitionTypes.cs:1958); no change_cell equivalent |
| #2 Cardinality | A SET of cells (CELLARRAY) simultaneously; collision tested against all |
Primarily one CellId; A4 CheckOtherCells adjacency is partial + dormant |
handoff §1 / A4 notes |
| #3 Collision path | Uniform sphere-sweep over all cells (terrain polys + room polys, same machinery) | Forked at cellLow >= 0x0100: cell PhysicsBSP (+ TryFindIndoorWalkablePlane synth-floor workaround) indoors vs SampleTerrainWalkable terrain-triangles outdoors |
FindEnvCollisions branch |
| #4 Building entry | Intrinsic — find_transit_cells→CBuildingObj::find_building_transit_cells (pc:318309) adds interior cells to the same CELLARRAY |
Bridge hook — CheckBuildingTransit promotes the CellId outdoor→indoor (0x90 stickiness workaround) |
CellTransit.CheckBuildingTransit |
When membership changes: retail — only on CObjCell::change_cell (pc:281192) at a portal-plane
crossing; between crossings the pointer is simply remembered. acdream — every tick, wherever the
sphere geometrically lands. That single distinction (remembered pointer vs recomputed scalar) is
why retail doorways are stable and ours ping-pong: a push-back across a CellBSP boundary that does
NOT cross a portal plane changes nothing in retail; acdream re-derives and flips to outdoor.
Precision (sharpens the target):
- acdream's re-derivation is seeded (FindCellSet seeded with
sp.CheckCellId), not from-scratch — i.e. a band-aided recomputation, not persistent state. - The persistence does not live in
find_cell_list(itnum_cells=0s + rebuilds every call,pc:308747). It lives incurr_cell+change_cell(pc:281192) + the transition's portal-crossing detection (find_transit_cellsexit-portal flag).find_cell_listonly supplies the current-first seed bias. ⇒ the port target ischange_cell+ crossing detection in the transition, not the pick.
4.5 THE FIX — STAGED (this is the fresh session's task; the 5ca2f44 pre-check is DROPPED)
Stage 1 — the flap's root-cause fix (do this first): port retail's persistent, multi-valued, portal-crossing membership.
- Make the player's cell sticky object state (home:
CellGraph.CurrCell) — mutated ONLY by achange_cell-equivalent when the transition detects a portal-plane crossing, NOT re-derived per tick fromFindCellSet. Retail anchors:CObjCell::change_cell @ 0x513390(pc:281192),CEnvCell::find_transit_cells @ 0x52c820(the exit-portal flag = the crossing signal),CPhysicsObj::SetPositionInternal @ 0x515330(readssphere_path.curr_cell). - Make doorway membership multi-valued (the
CELLARRAYset) so you're in outdoor+indoor simultaneously and never flip — collision already iterates candidates (CheckOtherCells/A4); wake that path and feed it the persistent set rather than a single re-derivedCellId. - Delete the
5ca2f44current-first pre-check (band-aid on the recomputation). Keep its regression test (TwoOverlappingCells_CurrentCellWinsTheStraddle) — it still guards a valid invariant. - This kills the ping-pong by construction AND moots #3's flap-face (sticky membership ⇒ no flip to "outdoor" at the threshold ⇒ the indoor BSP is always consulted there).
Stage 2 — full faithfulness (after Stage 1 lands + the flap is gone):
- #3 uniform collision: one sphere-sweep over all cells in the array (terrain polys + room polys,
same machinery); remove the
0x0100fork +TryFindIndoorWalkablePlanesynth-floor. Biggest piece (acdream terrain isn't polygons-in-a-BSP today — a real rearchitecture). - #4 intrinsic entry: building portals add interior cells to the array during the sweep
(
find_building_transit_cells), replacing theCheckBuildingTransitpromotion bridge + the0x90workaround.
The old §4.3 ordered-pick port is NOT Stage 1. Stage 1 is about where membership lives and when it changes (persistent state, portal crossings), not about how the per-tick pick is ordered.
- Thread the new ordered-collection type through the methods that build candidates:
BuildCellSetAndPickContaining,FindTransitCellsSphere,AddAllOutsideCells,CheckBuildingTransit(they currently takeHashSet<uint> candidates). Changing the type is the invasive part the user authorized. - Honest scope: the interior pick is fully verbatim-portable. The outdoor fallback
(the
gx/gyXY-column) stays an acdream adaptation — acdream landcells lack retail'sCLandCell::point_in_cell(they're a terrain grid). Mark it clearly. The flap is all interior/boundary, so the verbatim interior pick covers it. - Suggested collection: a small
CellArrayclass —List<uint>(order) +HashSet<uint>(O(1) dedup);Add(id)appends iff new; ordered enumeration;Contains;Count. Exposes its list asIReadOnlyCollection<uint>for theout cellSetreturn.
5. REPLACE the committed pre-check approximation (5ca2f44)
5ca2f44 added an explicit current-cell-first pre-check in BuildCellSetAndPickContaining
(before the interior-pass foreach): if the current cell is interior and its CellBSP contains
the center, return it. This achieves the property but NOT retail's ordered-array structure, and
it still diverges on the multi-neighbour edge (hash-order among non-current candidates). The user
wants this replaced by the verbatim ordered-CELLARRAY port (§4.3). When you implement the
ordered pick, delete the pre-check (it becomes redundant).
- Keep the regression guard test added in
5ca2f44:CellTransitFindCellSetTests.TwoOverlappingCells_CurrentCellWinsTheStraddle(two-direction[Theory]). It documents the current-cell-first invariant and passes under the verbatim port. Note: it does not go RED against the bug statically (the HashSet happens to enumerate the current cell first when the set is small/unchurned — see §7), so it's a guard, not the RED repro. The real verification is the harness + the visual flap gate.
6. THE WORKFLOW for the fresh session (this is a PHYSICS port)
Per CLAUDE.md's mandatory faithful-port workflow (the triangle-Z / frame-swap lessons):
- Grep named — DONE.
find_cell_listfound at pc:308742. - Read decomp. §4.2 has the
find_cell_listpick (CONTEXT only). The Stage-1 target is the PERSISTENCE mechanism — readchange_cell(pc:281192),find_transit_cells(the exit-portal crossing = when membership changes),SetPositionInternal(readssphere_path.curr_cell). - WRITE PSEUDOCODE (the step skipped this session): translate retail
find_cell_list's candidate-build + ordered pick to readable pseudocode indocs/research/*_pseudocode.mdbefore porting. This catches misreads. - PORT FAITHFULLY line-by-line (§4.5 Stage 1): persistent
curr_cellstate mutated only by achange_cell-equivalent at a portal crossing + multi-valuedCELLARRAYmembership. Same control flow as retail. Don't "improve." (NOT the §4.3 pick-ordering — superseded.) - CONFORMANCE TEST: extend
CellTransitFindCellSetTests— a multi-neighbour straddle where the current cell wins by order (not just by containment), and an indoor↔outdoor straddle (vestibule stays vestibule while it contains you). - VERIFY (no manual probe walks): the deterministic harness (
CellTransitFindCellSetTests,CellarUpTrajectoryReplayTests,DoorBugTrajectoryReplayTests), then run the FULL physics suite to see breakage (dotnet test tests/AcDream.Core.Tests), then the visual flap gate (user walks normally;ACDREAM_PROBE_CELLauto-logs so you can confirm the transition count drops 59 → ~6–8 without asking them to do anything extra).
Superpowers: you may go straight to superpowers:writing-plans → superpowers:executing-plans
for the port (the design is clear — a heavy brainstorming pass is probably unnecessary). Use
superpowers:systematic-debugging if the flap doesn't fully clear and you need to chase a residual.
Use superpowers:test-driven-development for the conformance tests.
7. WHY the static unit test alone won't catch it (important nuance)
The pre-check / the bug interplay: .NET HashSet<uint> with a small, unchurned set tends to
enumerate in insertion order, and the current cell is added first — so a static single-tick
test where the current cell contains the center already returns the current cell (the
TwoOverlappingCells guard PASSES even against the unfixed pick). The production ping-pong is
dynamic — it arises from (a) the candidate set's contents churning tick-to-tick (reordering
the enumeration), and/or (b) the foot position genuinely oscillating across a boundary (the stairs
Z-jitter, §8). The ordered CELLARRAY removes (a) by construction (deterministic current-first
order every tick). (b) is the separate physics issue. ⇒ verify dynamically (harness + visual),
not just by the static guard.
8. The SEPARATE stairs-physics suspicion (don't conflate with the pick)
On the stairs the foot Z oscillated ~0.2 m/tick while membership flipped 0175↔0174. The room
flips were at constant Z (pure pick). So:
- The room/vestibule flips → fixed by the verbatim current-first pick (this task).
- The stairs flip may be a separate physics-movement instability (step-up/step-down on the
cellar stairs/ramp — the #98 family: "stuck at the last step",
Transition.AdjustOffset/DoStepUp). The current-first hysteresis will dampen it if the wobble stays inside the current cell's BSP, but if the Z-oscillation is large enough to cross the cell boundary it will persist. - If the stairs still flap after the membership port lands and the room/door flaps are gone,
that's the physics-movement target (a #98-area follow-up) — diagnose it the same evidence-first
way (the existing
ACDREAM_CAPTURE_RESOLVE+ the trajectory-replay harness; the user's cellar-stairs is the repro). Do not block the membership port on it.
9. KEEP / DON'T-REDO (avoid re-litigating settled work)
KEEP (correct, do not reopen for the flap):
- All R1 render code:
InteriorEntityPartition,InteriorRenderer(per-cell DrawInside loop), the binary decision inGameWindow.OnRender, theWbDrawDispatchergate fix. The cellar seals — render is correct. PortalVisibilityBuilder,ClipFrameAssembler/ClipFrame,EnvCellRenderer,TerrainModernRenderer, the WB mesh pipeline. The render design spec + R1 plan.- The swept-membership chain (A1):
ResolveWithTransitionreturningsp.CurCellId. Do NOT revert to a staticResolveCellIdre-derive. - The
5ca2f44regression test (TwoOverlappingCells_CurrentCellWinsTheStraddle).
DON'T:
- Don't reopen R1's render code chasing the flap — the flap is membership.
- Don't re-port rendering from the decomp (use the WB pipeline — CLAUDE.md).
- Don't add a render-side debounce/grace-period for the flap (bandaid — forbidden).
- Don't ask the user for manual probe walks (diagnose from existing data + harness; verify visually).
10. TEST STATE (baseline for regression judgement)
- Pre-existing Core failures (NOT yours — verified): the handoff's "5" — 2 step-up gaps (incl.
an A6.P4 door regression) + 3 door-collision apparatus / A6.P5. The 2
DoorBugTrajectoryReplayTestsfailures (TransientState live=0x87 harness=0x83, e.g.LiveCompare_DoorBlocksFromOutside_Tick22760) were verified pre-existing this session (they fail without the membership change too). Plus the documented PhysicsResolveCapture/PhysicsDiagnostics static-leak flakiness (8–19 failures across runs of identical code). ⇒ the deterministic membership net (CellTransit|FindEnvCollisions|CellGraph|Doorway|Cellar|DoorBug) is the reliable signal: it was 66 pass + 2 pre-existing DoorBug with the pre-check. tests/AcDream.App.Tests: 174 green (incl.InteriorEntityPartitionTests×3, the flippedEntityClipTestsgate test).- Breakage is authorized (§0): when the verbatim port lands, run the full physics suite, diff the failure set against this baseline, and fix genuinely-new breakage (the port may legitimately change membership-dependent test expectations — update them with retail-cited reasoning, don't pin wrong values).
11. After the membership port: the remaining render arc
Once the flap is gone and the seal holds, resume the R1→R7 plan (design spec §7):
- R1b — per-cell particles (#104): "particles bleed through the ground when looking out the
door" — Scene-pass particles aren't cell-clipped (needs a cell link on
ParticleEmitter; the per-cellDrawInsideloop makes this tractable). - R2 — outside-looking-in (
DrawPortal): "interior walls are transparent when looking in a window/door from outside" — no outdoor→interior portal render yet. - R3 — dungeons. R4 — polish (the
CullMode.Landblock→Nonewinding; remove dormant WB two-pipe scaffoldingBuilding/BuildingLoader; conformance). - The cottage seal (R1) is not signed off until the flap is gone — the membership port is the R1 gate's remaining blocker.
12. KEY FILES + ANCHORS (quick index)
MEMBERSHIP (the task)
src/AcDream.Core/Physics/CellTransit.cs
FindCellSet (entry, ~388 single-sphere, ~412 multi-sphere)
BuildCellSetAndPickContaining (the candidate build + pick — THE function to port)
~433 candidates = new HashSet<uint>() ← the unordered set to replace
~447 candidates.Add(currentCellId) ← current added first (indoor seed)
~520 the 5ca2f44 pre-check ← DELETE when the ordered pick lands
~544 foreach interior pass (the pick) ← port verbatim (ordered, current-first)
~558 gx/gy outdoor fallback ← acdream adaptation (landcells lack point_in_cell)
FindTransitCellsSphere / AddAllOutsideCells / CheckBuildingTransit ← take `HashSet<uint> candidates` (re-type)
src/AcDream.Core/Physics/TransitionTypes.cs:1958 ← FindEnvCollisions calls FindCellSet (the swept membership)
src/AcDream.Core/Physics/PhysicsEngine.cs:608/880/901← ResolveWithTransition returns swept sp.CurCellId
src/AcDream.App/Input/PlayerMovementController.cs:1296 ← UpdateCellId(resolveResult.CellId, "resolver")
tests/AcDream.Core.Tests/Physics/CellTransitFindCellSetTests.cs ← conformance home (+ the kept guard)
(line numbers ≈ — shifted by 5ca2f44's ~22 lines; grep by method name)
RETAIL DECOMP (the verbatim source)
CObjCell::find_cell_list 0x52b4e0 pc:308742 (pick = 308788–308825; add_cell @308766; point_in_cell vtable[0x84] @308810; find_transit_cells vtable[0x80] @308782)
CEnvCell::find_transit_cells 0x52c820 pc:309968
acclient.h: CELLARRAY 31574 ; CELLINFO 31925 ; SPHEREPATH 32625
Ghidra MCP (port 8081, patchmem.gpr) for /decompile_function on add_cell if exact dedup wanted.
RENDER (correct — context only)
src/AcDream.App/Rendering/InteriorRenderer.cs ← per-cell DrawInside loop
src/AcDream.App/Rendering/InteriorEntityPartition.cs ← 3-bucket entity split
src/AcDream.App/Rendering/GameWindow.cs (OnRender ~7530)← the binary decision
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:~1744 ← EntityPassesVisibleCellGate (gate fix)
13. RUNNING THE CLIENT + apparatus (no manual probe walks)
Per CLAUDE.md "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_CELL="1" # auto-logs [cell-transit]; lets you confirm the count drops, no manual walk
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 (
Select-String/ ripgrep--encoding utf-16-le). - A client from THIS session may still be running (graceful close clears ACE in ~3–5s; a hard kill leaves the session stuck ~3 min — see CLAUDE.md). Close gracefully before relaunching.
- Verify the flap visually (user's eyes) + the
[cell-transit]count. Walk: outside0031→ door0170→ room0171→ stairs0175→ cellar0174, and back. Room/door flap should be GONE.
14. PICKUP PROMPT (copy-paste for the fresh session)
VERBATIM PORT of retail CObjCell::find_cell_list's containing-cell pick — to fix the cell-MEMBERSHIP
ping-pong that the R1 render redesign exposed (the cottage "flap"). 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).
AUTHORIZATION (user, explicit): you may BREAK ANY physics/movement code or tests to get the engine +
membership working faithfully — breakage is OK, fix later. Do the faithful line-by-line port; don't
tiptoe around the #98-area logic. Run the physics suite to SEE what breaks. The user is tired of
probe walks — diagnose from existing data + the deterministic trajectory-replay harness; verify with
a normal VISUAL test (their eyes) + the auto-logging ACDREAM_PROBE_CELL.
READ FIRST (in order):
1. docs/research/2026-06-02-membership-verbatim-port-handoff.md (THIS handoff — diagnosis §3; THE
DEEPER ROOT + the STAGED fix §4.4-4.5 [the §4.3 pick-ordering framing is SUPERSEDED]; KEEP §9).
2. docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md (the render redesign — context; render is CORRECT + downstream of membership).
3. docs/superpowers/plans/2026-06-02-render-r1-per-cell-drawinside.md (R1 plan — what shipped).
THE ROOT (handoff §4.4 — the pick-ordering framing is SUPERSEDED): membership is *state* in retail (a
persistent curr_cell pointer mutated ONLY by change_cell at a portal crossing) but a per-tick
*recomputation* in acdream (CellId re-derived from FindCellSet geometry every tick). That
recompute-vs-remember is the ping-pong root. A pick-ordering fix only makes the recomputation
stickier — too shallow. DROP the 5ca2f44 pre-check.
THE JOB — STAGE 1 (the flap fix): port retail's PERSISTENT, MULTI-VALUED, PORTAL-CROSSING membership.
- Make the player's cell sticky OBJECT STATE (home: CellGraph.CurrCell) — mutated ONLY by a
change_cell-equivalent when the transition detects a portal-plane crossing, NOT re-derived per tick.
Anchors: change_cell 0x513390 (pc:281192), find_transit_cells 0x52c820 (exit-portal flag = the
crossing signal), SetPositionInternal 0x515330 (reads sphere_path.curr_cell).
- Make doorway membership MULTI-VALUED (the CELLARRAY set) so you're in outdoor+indoor at once and
never flip; wake the dormant CheckOtherCells/A4 multi-cell collision and feed it the persistent set.
- DELETE the 5ca2f44 current-first pre-check (band-aid). KEEP its regression test
(TwoOverlappingCells_CurrentCellWinsTheStraddle).
STAGE 2 (after Stage 1 + flap gone): #3 uniform collision (one sphere-sweep, terrain polys + room
polys; remove the 0x0100 fork + TryFindIndoorWalkablePlane) + #4 intrinsic building entry
(find_building_transit_cells adds cells to the array; remove CheckBuildingTransit + 0x90 workaround).
WORKFLOW (physics port — mandatory): grep named → read decomp (change_cell 281192 + find_transit_cells
+ SetPositionInternal — the persistence mechanism) → WRITE PSEUDOCODE (docs/research/) → PORT
FAITHFULLY line-by-line → CONFORMANCE TEST → run full physics suite (see breakage vs the §10 baseline;
fix new breakage — breakage is AUTHORIZED) → VISUAL flap gate (room/door flap GONE; [cell-transit]
count 59→~6-8). Use superpowers:writing-plans → executing-plans; test-driven-development for conformance.
PROVEN, DON'T RE-LITIGATE: the flap is MEMBERSHIP not render (cellar seals when membership is stable;
flap tracks the [cell-transit] ping-pong). R1 render is correct — do NOT reopen it. The 2 DoorBug
TransientState failures are PRE-EXISTING (verified). The stairs flip additionally shows the foot Z
oscillating ~0.2m/tick = a SEPARATE physics issue (#98 family, §8) — likely mooted by sticky
membership; if it persists after Stage 1, that's the next target; don't block on it.
GOAL: persistent portal-crossing membership (Stage 1) → the R1 seal holds with no flap → Stage 2
(uniform collision + intrinsic entry) → resume R1b (particles) → R2 (outside-looking-in) per the design spec.