Root cause (oracle: CLandCell::point_in_cell :316941 = terrain-poly only;
find_cell_list null-result keep-curr pc:308788-308825; CEnvCell::
check_building_transit :309827 = sphere_intersects_cell per portal-adjacent
cell): retail KEEPS curr_cell when nothing contains the centre — including
inside a house's containment gaps. Our 6dbbf95 escape hatch instead demoted
any hydrated indoor claim the sphere no longer overlaps to the outdoor
column; at the A9B3 hill cottage's real interior gap this stranded the
player outdoor-classified deep indoors, where re-promotion is portal-
adjacent-only (retail-identical) -> the outdoor flood rendered the interior
transparent (the user's "sometimes transparent" walk).
The hatch's actual target - poisoned (cell, position) SAVES - has been
handled at the SNAP by PhysicsEngine.Resolve's AdjustPosition validation
since #107/#111, so the per-tick pick reverts to retail semantics:
1. lateral recovery first - when the sphere no longer overlaps the claim,
search the claim's stab list for a containing cell (retail
find_visible_child_cell :311444, the same recovery AdjustPosition uses);
the #111 adjacent-claim shape now self-heals laterally (dat-backed test:
pick(seed 0x172) at a 0x171-interior point -> 0x171);
2. else KEEP curr_cell (retail null-result).
Two old tests asserting the hatch demote rewritten to the retail semantics
(tests-can-codify-bugs); P1 retail-golden conformance gates explicitly green
(FindCellListConformance + ThresholdPortalCrossing + CottageDoorway +
CameraCornerSeal = 11/11). New Issue112MembershipTests: the lateral-recovery
fact + a DocumentsResidual fact pinning the remaining at-doorway gap demote
(via the NORMAL outdoor-candidate path; open oracle read = retail's
add_all_outside_cells gate in CEnvCell::find_transit_cells pc:317499 -
sphere-proximity vs graph-reachability). Core 1383 + 4 pre-existing #99
failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two grounding selection rules failed against live ACE restores before the
right one: (1) first-hit SampleFloorZ returned 0x171's 99.475 ceiling TOP face
over its 94.0 floor (issue111-verify2.log) - the player committed onto the
roof level, and the session's heartbeats poisoned ACE's save with z=99.475;
(2) nearest-to-reference self-confirmed that poison (the reference SAT on the
ceiling face, issue111-verify3.log). Root insight: ceiling/roof top faces are
upward-facing and XY-projectable - geometrically indistinguishable from
floors in the render-ish CellSurface soup. The PHYSICS walkable set (plane
normal.Z >= PhysicsGlobals.FloorZ over the claim's Resolved cell-local
polygons - retail BSPTREE::find_walkable's filter) contains only real floors:
PhysicsEngine.WalkableFloorZNearest transforms into the cell frame, drops on
each walkable plane under the XY, picks nearest the reference.
Verified live (issue111-verify4.log): ACE restored the roof-poisoned
(0xA9B40171, z=99.475); the snap validated the claim and grounded to
z=94.000 - the first fully clean indoor login of the arc:
[snap] claim=0xA9B40171 VALIDATED -> grounded to its walkable floor z=94.000
[cell-transit] 0x00000000 -> 0xA9B40171 pos=(155.525,12.416,94.000)
Baseline: Core 1381 + 4 pre-existing #99 failures + 1 skip; App/UI/Net green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The [snap] apparatus (issue111-snap1.log) caught the mechanism live: ACE
restored a CLEAN pair (0xA9B40171, on-floor) which AdjustPosition validated -
and the legacy Resolve then committed 0xA9B4013F instead: its bestCell floor-
pick scans EVERY CellSurface in the landblock (123 at Holtburg) for "any floor
under this XY nearest currentZ" and breaks same-height ties by iteration
order. The wrong cell then fails containment on the first movement -> outdoor
demote inside the building -> the #111 transparent interior. This free-pick
also explains the earlier "committed verbatim" mystery (the winning tie
happened to echo the input pair) AND seeded the ACE poison loop: our outbound
heartbeats reported the clobbered cell, ACE persisted it, and the NEXT login
inherited it (this run's [spawn-adjust] rejecting 0xA9B4013F is exactly that
echo coming back).
Fix (retail SetPositionInternal shape): when AdjustPosition VALIDATED an
indoor claim, the cell choice is settled - the snap grounds Z onto the
validated cell's own floor (find_valid_position's settle role, :283426) and
returns; it never re-picks the cell from floor geometry. Claims whose cell has
no own floor surface under the XY (thresholds, stair lips) fall through to
the legacy path unchanged; mover-shaped calls (delta != 0, tests) untouched.
[snap] diagnostic kept (snaps only - one line per login/teleport).
Baseline: Core 1381+4 pre-existing #99 failures+1 skip; App/UI/Net green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-run finding (wedge-107-gate-indoor-login.log + dat scan): ACE saved the
cell one room off (claim 0xA9B40172, position inside 0xA9B40171 — adjacent
rooms). FindVisibleChildCell corrects this fine when hydrated (proven by the
new AdjacentRoomClaim regression test), but the live entry committed the raw
claim: IsSpawnCellReady's "any cell struct in the landblock => claim is bogus,
proceed" disambiguator observed the MID-POPULATION state (interiors hydrate in
id order on the background worker; the render-thread predicate read the cache
mid-loop) and opened the gate before the claim and its stab neighbors were
cached. AdjustPosition then saw a null cell struct and silently passed the
claim through; the first movement demoted the player to outdoor inside the
house — the user-visible "transparent interior, see straight through walls"
(render is downstream of membership: an outdoor-classified viewer only sees
the interior through the doorway flood).
Fix: the hold now waits for THE CLAIMED CELL's struct, full stop
(IsSpawnCellReady simplification; HasAnyCellStructInLandblock removed).
Claims that can never hydrate are filtered by GameWindow against the dat's
LandBlockInfo.NumCells range (memoized IsSpawnClaimUnhydratable), and
PhysicsEngine.Resolve carries a loud lost-cell-equivalent safety net: an
indoor claim with NO cell struct AND NO CellSurface floor data demotes to the
outdoor landcell with a [spawn-adjust] line instead of committing raw
(retail GotoLostCell :283418; documented divergence). Partial hydration
(CellSurface present, struct pending) keeps the legacy floor-snap behavior —
HasCellSurface uses the file's masked-low-word norm so bare-id fixtures and
full-id production both resolve.
Baseline restored: Core 1381 (+4 new #107 conformance tests) + 4 pre-existing
#99-era failures + 1 skip; App 223 / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (capture resolve-107-login1.jsonl + dat conformance scan): ACE
restored the player with a POISONED (cell, position) pair — cell 0xA9B40162
(one building) with a position inside 0xA9B40171 (a different building 55 m
away). Our entry snap trusted the claim verbatim: the player stood
fake-grounded for minutes (isOnGround passthrough, no contact plane, no
walkable polygon — zero-move resolves short-circuit), the FIRST movement input
ran a real transition, the pick demoted the indoor claim to outdoor
mid-building, and the player fell 2.4 m through the cottage floor onto the
terrain underneath — wedged inside the building shell. The second wedge shape
(flood-fix-gate2.log) was the PortalSpace freeze: the teleport-arrival
detection gated on `differentLandblock || farAway>100m`, an invented
heuristic — ACE's same-landblock short-hop corrections matched neither, so
PortalSpace never exited and movement input stayed frozen all session.
Four legs, all retail-anchored:
1. PhysicsEngine.Resolve (the player snap path: login entry + teleport
arrival) now runs AdjustPosition first — retail SetPositionInternal step 1
(acclient :283892, AdjustPosition :280009): validate/correct the claimed
cell from the foot-sphere center BEFORE any physics. Corrections log one
[spawn-adjust] line.
2. AdjustPosition's previously-deferred indoor seen_outside →
adjust_to_outside sub-fallback (:280037-280046) is completed; CellPhysics
gains the SeenOutside flag (dat EnvCellFlags.SeenOutside) cached in
CacheCellStruct. The camera path does not reach this sub-branch in the
gated scenarios (CameraCornerSealReplayTests green).
3. PortalSpace arrival = ANY player position update (holtburger PlayerTeleport
handler conformant; recenter still only on landblock change). Verified
live: ACE sent a same-lb dist=69.8 correction that the old gate would have
frozen on — it now completes.
4. Outbound wire (cell, position) pairs are now SELF-CONSISTENT: derive the
landblock frame from the resolver's full cell id instead of welding a
position-derived landblock onto its low word — the old composition could
write exactly the poisoned pair shape into ACE's character save. Plus the
#106-gate-2 hold extension: an indoor spawn claim waits for the claimed
cell's hydration (IsSpawnCellReady) so the validation can act — the async
equivalent of retail's synchronous cell load.
Live verification (wedge-107-verify1.log): entry clean; ACE's same-lb teleport
correction completed (old code: permanent freeze); the teleport destination
itself carried ANOTHER poisoned claim (0xA9B40150) which [spawn-adjust]
corrected to 0xA9B40019; player fully controllable, walking across landcells.
3 new dat-backed conformance tests pin the poisoned-pair facts
(Issue107SpawnDiagnosticTests). Baseline: 1380+4 pre-existing #99-era
failures+1 skip / 223 / 420 / 294.
Pending user gate: park indoors, log out gracefully, relaunch — expect a clean
indoor spawn standing on the interior floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: TextureAtlasManager.AddTexture only STAGES texture content (PBO
write + ManagedGLTextureArray._pendingUpdates); the actual TexSubImage3D
copies + mipmap regeneration happen in ProcessDirtyUpdates, which WB drives
once per frame via ObjectMeshManager.GenerateMipmaps() from its render loop
(WB GameScene.cs:975, just before the opaque pass). GameScene is the file we
replaced with GameWindow, so the call site was silently dropped — staged
updates only reached the GPU as a side effect of PBO growth (UpdateLayerInternal
flushes pending updates before orphaning the PBO). Every layer staged after an
array's LAST growth kept undefined TexStorage3D content behind a valid,
resident bindless sampler handle: white/garbage walls, zh==0, dat tripwires
silent — exactly the #105 signature. Only ObjectRenderBatch.BindlessTextureHandle
consumers are affected (EnvCellRenderer cell shells = indoor walls); entities
resolve via TextureCache (immediate TexImage2D) and terrain via TerrainAtlas
(immediate GenerateMipmap), which is why only indoor walls ever struck.
Fix: WbMeshAdapter.Tick() now calls _meshManager.GenerateMipmaps() after the
staged-upload drain — Tick runs before all draw passes (GameWindow OnRender),
the exact WB-equivalent position.
Evidence (ACDREAM_PROBE_TEXFLUSH=1 apparatus, kept env-gated):
- pre-fix (texflush-prefix.log): pending updates climb 0->48->...->142 and
park at 126 across 34/34 atlas arrays at standstill, forever (19 heartbeats);
brief dips only at PBO-growth crossings — the broken contract live.
- post-fix (texflush-postfix.log): every line after=0 — staged updates drain
the same frame, all 34 arrays clean.
Intermittency explained: background decode-completion order shuffles which
textures land in the never-flushed tail; whether a visible wall samples one is
per-run luck. Also explains the #110 correlation: znear=0.1 makes close-up
geometry newly visible -> more prepare/upload pressure indoors -> bigger tail
-> higher strike probability. The near plane is mechanism-innocent (re-land
follows as its own commit).
Baseline maintained: App 223 / UI 420 / Net 294 / Core 1377 green + 4
pre-existing #99-era failures + 1 skip; CornerFloodReplayTests (5) and
CameraCornerSealReplayTests (2) gates green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decisive probe between the two surviving suspects from the 2026-06-09
building-flood-merge handoff (docs/research/2026-06-09-flap-outdoor-fullworld-
building-flood-merge-handoff.md section 1), gated by ACDREAM_PROBE_CLIPROUTE=1,
all print-on-change:
- [clip-route] (RetailPViewRenderer.DrawLandscapeThroughOutsideView): the
outside slice slot + NDC AABB + planes, the CellIdToSlot routing table, the
region-SSBO bytes DECODED at the routed slot, and the terrain-UBO head —
captured after SetTerrainClip + UploadClipFrame + SetClipRouting, i.e.
exactly what the landscape draws consume. Pins/refutes suspect (b) and the
slot-repack half of suspect (a).
- [clip-route-disp] (WbDrawDispatcher.Draw, routed draws only): per-slot
instance histogram exactly as staged for binding=3 plus the count of
entities dropped by ResolveSlotForFrame CULL. Pins/refutes the
instance-routing half of suspect (a).
- [clip-route-scis] (GameWindow.DrawRetailPViewLandscapeSlice): the ACTUAL GL
scissor enable + box read back right after BeginDoorwayScissor — the whole
landscape pass (sky + terrain + outdoor entities + player) draws inside this
box, so a doorway-sized box here IS the full-world kill by construction.
Code-reading findings recorded while building the probe: the landscape pass is
scissored to slice.NdcAabb end-to-end (GameWindow.cs DrawRetailPViewLandscapeSlice),
and ResolveEntitySlot CULLs server entities with null ParentCellId while routing
is active — both now directly observable under the probe.
Throwaway apparatus — strip once §4 ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two probe additions from the §4 outdoor full-world flap investigation
(world turns to fog-tinted clear color at specific outdoor spots;
strobes then holds; camera rotation recovers; forward-walk triggers):
- [pv-input] gains terrZ=/eyeAbove= (terrain height under the camera
eye) — used to REFUTE the buried-eye hypothesis (eyeAbove stayed
+1.6..2.1 m through every held-flap block).
- [gl-state] (ACDREAM_PROBE_GLSTATE=1): snapshots the GL fixed-function
state entering the world passes (depth, blend, cull, scissor + box,
viewport, FBO, color/clip masks, glGetError), printing on change.
Delivered the frame-exact flap-onset marker: the leftover scissor box
flips from full-screen to a drifting 9x21 px doorway footprint at the
exact frame the nearby building flood merges in (pv-input flood 1->5).
Both probes are gated and zero-cost when off. Strip with the rest of
the §4 apparatus when the flap ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-2 fallout chain: session 1's bare-id wedge poisoned ACE's save (the
client reported garbage cells while walking through walls), so session 2
logged in with (cell=0xA9B4013F inn interior, pos=(91.4,32.7)) — a
position that cell does not contain. Probe evidence: exactly one
[cell-transit] line all session; the player free-fell into an empty
world. Two holes, both fixed at the root:
1. CellTransit pick escape hatch — restores the #83/A1.7 + #90
verification that lived in PhysicsEngine.ResolveCellId before the
collide-then-pick rewrite moved membership into
BuildCellSetAndPickContaining: an indoor current cell that IS
hydrated but whose CellBSP no longer overlaps ANY part of the foot
sphere is a bogus claim (corrupt save, or walked out through an
unblocked gap). The portal BFS can never reach an exit portal from a
cell the sphere isn't in, so no candidates exist and the claim held
forever — wedging collision (ShadowObjectRegistry's #98 gate reads
"indoor primary" -> outdoor object sweep skipped), wall BSP, terrain,
and the render root. The pick now demotes to the outdoor column under
the sphere centre (the LandDefs.AdjustToOutside result already
computed for the pick — cross-block safe). Sphere-overlap
(BSPQuery.SphereIntersectsCellBsp, pseudo_c:317666 -> :323267), NOT
point-in: doorway push-back leaves the centre a few cm outside while
the sphere still overlaps — no demotion, #90's ping-pong stays dead.
An unhydrated cell cannot be verified — stale beats null while
streaming hydrates (retail-equivalent: stale curr_cell kept when the
pick finds nothing).
2. PlayerModeAutoEntry spawn-ground hold — player-mode entry now waits
for the terrain under the spawn position to stream in
(isSpawnGroundReady predicate, K.2 pattern). Entering earlier
integrates gravity against an empty world: indoor-claimed spawns got
no floor from any source and free-fell into the void; outdoor spawns
raced hydration by ~1s every login. Retail never has this state (it
loads cells synchronously) — the hold is the async-streaming
equivalent of that invariant. With the hold, the entry snap
(Resolve, stepUp=100) runs against hydrated cell floors + terrain
and re-seats a corrupt save's claim immediately.
Tests: IndoorSeed_SphereFullyOutsideHydratedCell_DemotesToOutdoorColumn
(the gate-2 wedge shape, red pre-fix), straddle + no-BSP guards (the #90
hysteresis and stale-beats-null), TryEnter_Armed_SpawnGroundNotReady_
DoesNotFire. Full suite: 294+218+420 green; Core 1375 green + the same
4 pre-existing door/#99-era failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #106 live gate run was sabotaged by a pre-existing bug the corrective
ACE teleport exposed: PhysicsEngine.Resolve (the legacy Phase-D resolve,
still used by the teleport-arrival snap at GameWindow.cs:4869 and the
player-mode-entry snap at :11295) returned BARE low-word cell ids on
every computed exit (ComputeOutdoorCellId, bestCell.CellId & 0xFFFF,
nextCellIndex, enterCellIndex). The teleport committed 0x0000013F into
PlayerMovementController.CellId, and a bare indoor id wedges the entire
membership chain:
- GetCellStruct(0x0000013F) misses (cells are keyed full-prefix) -> no
indoor wall BSP -> walk through walls;
- the b3ce505#98 gate reads "indoor primary" -> outdoor object radial
sweep skipped -> NO object collision anywhere in the world;
- BuildCellSetAndPickContaining early-returns an unresolvable id forever
(block 0x0000 is a real far-NW map block) -> membership frozen;
- render root never resolves -> interiors draw empty when stepping in.
Probe evidence: probe-cell-106-gate.log has exactly 2 [cell-transit]
lines for the whole session, both reason=teleport — the second one
(0xA9B3003C -> 0x0000013F) is the wedge. This is the L.2e "player CellId
tracked as bare low byte" finding (2026-05-12) finally biting; prefix
survival until now was a race artifact — Resolve only preserved the full
id when the landblock had not streamed in yet (passthrough exit), which
is why login snaps usually came out prefixed.
Fix: capture the matched landblock's key in Resolve's containment loop
and return lbPrefix | (targetCellId & 0xFFFF) on the computed exit —
the same full-32-bit convention Resolve's own doc comment states for
its inputs, and what both production callers (player snaps) require.
The passthrough exits (no landblock / step-reject) still return the
caller's id unchanged.
Tests: Resolve_IndoorStay_ReturnsFullPrefixedCellId (the teleport
shape, red pre-fix) + Resolve_OutdoorStay_ReturnsFullPrefixedCellId;
Resolve_LeaveIndoorCell_TransitionsToOutdoor's unmasked
`CellId < 0x100` assertion codified the bare behavior — now masked +
asserts the prefix. Full suite: 294+218+420 green; Core 1371 green +
the same 4 pre-existing door/#99-era failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The player's outdoor cell froze at the last in-block cell the moment they
walked over a landblock boundary (10,449-frame playerCell freeze in the
2026-06-09 capture; whole neighbouring-block interiors unenterable, plus
the running-distortion from the stale render anchor). Root cause: the
add_all_outside_cells port clamped BOTH the candidate proposal and the
find_cell_list containing-cell pick to the current landblock's 8x8 grid,
in a frame that silently assumed the current block sits at world origin.
One step over the line -> zero candidates -> FindCellSet returns
currentCellId forever.
Retail has no such clamp. Its cell math runs in a GLOBAL landcell grid
(lcoord 0..2039 spanning the map): get_outside_lcoord = blockid_to_lcoord
+ floor(blockLocalPos/24) with no bounds besides the map edge, and
lcoord_to_gid re-derives the landblock id from the lcoord's upper bits —
crossings are inherent, never special-cased.
The fix, decomp-cited throughout:
- New AcDream.Core.Physics.LandDefs: in_bounds (pc:68509),
blockid_to_lcoord (pc:68520), inbound_valid_cellid (pc:163438),
gid_to_lcoord (pc:163500), lcoord_to_gid (pc:171859),
get_outside_lcoord (pc:438690), adjust_to_outside (pc:438719).
Cross-checked against ACE LandDefs.cs; three artifacts documented and
avoided: BN's int8_t mis-render of block_y, BN's dropped 192f
BlockLength constant, and ACE add_cell_block's "FIXME!" same-block
guard (an ACE divergence, not retail).
- CellTransit.AddAllOutsideCells rewritten as the faithful sphere
variant (pc:317499 @0x00533630): adjust_to_outside re-seats the
(cell, position) pair cross-block, check_add_cell_boundary (pc:317229)
adds up to 3 neighbours by global lcoord, add_outside_cell (pc:317056)
has no same-block filter. adjust_to_outside failure breaks the sphere
loop (pc:533699 verbatim).
- BuildCellSetAndPickContaining: the outdoor containing-cell pick is now
the global XY-column under the sphere centre (AdjustToOutside), not
the [0,8)-clamped current-prefix reconstruction. Interior-wins order
and current-cell-first hysteresis unchanged.
- World->block-local frame conversion via the landblock origin already
registered in CellGraph (new TryGetTerrainOrigin); Zero fallback
preserves the legacy anchor-block assumption for unregistered terrain.
- Cross-landblock building entry comes free: the candidate snapshot now
contains neighbour-block landcells, so GetBuilding/CheckBuildingTransit
fire for cottages across the line (the capture's one failing entry).
Investigated FIRST per the pickup brief: the b3ce505#98 stopgap gate is
definitively exonerated — it is a collision-object query gate that fires
only for indoor primary cells; no membership path touches
ShadowObjectRegistry.
Tests: 31 new (25 LandDefs conformance incl. capture-geometry goldens
0xA9B40031 -> 0xA9B30038/0xA9B30034 and the northbound return; 4
AddAllOutsideCells cross-block; 3 FindCellSet membership goldens incl.
the non-anchor-frame origin conversion). Full suite: 294+218+420 green;
Core 1369 green + the 4 pre-existing door/#99-era failures + 1 skip
(unchanged from baseline).
Pseudocode + artifact notes:
docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md.
Remaining acceptance: live boundary walk with ACDREAM_PROBE_CELL=1
(ISSUES.md #106).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.
- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
(load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).
Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.
Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail SmartBox::RenderNormalMode (0x453aa0:92665) branches DrawInside vs the
outdoor LScape::draw on is_player_outside (the PLAYER's cell, 0x451e80), then
roots DrawInside at the VIEWER cell. acdream keyed the whole branch off the
camera cell (clipRoot = visibility.CameraCell), so a 3rd-person chase camera
lagging in a doorway AFTER the player stepped outside took the DrawInside path
rooted at the threshold cell, where the exit-portal flood degenerates: grey
world + entities-through-walls. Now ShouldRenderIndoor(playerCellId,
viewerCellResolved) gates the branch on the player; the DrawInside root stays
the viewer cell (handoff invariant preserved).
SCOPE / HONESTY: this REDUCES the player-OUTSIDE doorway grey (visual-confirmed
reduced a lot) but does NOT fix the deeper symptom: when the player is in one
interior cell (cellar 0174) and the camera is in another (room 0171), the flood
roots at the camera cell and does NOT seal the player's cell, so the cellar
floor / interior walls drop to grey. That is the KNOWN R1-completion problem
(2026-06-05 Residual A handoff + 2026-06-02 design doc section 3: a
SHELL-SEALING / wrong-flood-root bug), not this branch.
Tests: Core 1331p / 4f (documented) / 1s, App 187p, build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete Render Residual A's faithful port: PhysicsCameraCollisionProbe.SweepEye
now mirrors SmartBox::update_viewer (acclient_2013_pseudo_c.txt:92761) end-to-end:
- Start cell (pc:92824-92844): indoor (>=0x100) seats the sweep at the head-PIVOT
via PhysicsEngine.AdjustPosition (the cellar-lip case — feet in the low connector,
head up at floor level); outdoor keeps the player cell.
- Sweep pivot -> sought-eye from the seated start cell (unchanged 0x5c viewer flags).
- Success (pc:92870): set_viewer(curr_pos), viewer_cell = curr_cell.
- Fallback 1 (pc:92878): AdjustPosition(sought_eye).
- Fallback 2 / no-cell (pc:92775, 92886): snap to player, viewer_cell = null. This
also makes cellId==0 faithful (was returning the desired eye; retail snaps to
player_pos) and adds the playerPos arg to ICameraCollisionProbe.SweepEye.
Supporting: ResolveResult.Ok surfaces FindTransitionalPosition's return (retail
find_valid_position != 0, pc:273898) so SweepEye knows when to fall back.
TDD: 11 new tests (FindVisibleChildCell 4, AdjustPosition 3, ResolveResult.Ok 2,
SweepEye orchestration 2). The seating test's RED proved the sweep does NOT auto-
advance feet->room, so the pivot-seated start cell is genuinely decisive. Core
1326 pass / 4 documented-fail / 1 skip; App 179 pass / 0 fail. No regression.
Per the live-capture finding, the visible payoff is the cellar-corner (point 3);
the cottage-room bluish void stays for residual C. Spec:
docs/superpowers/specs/2026-06-05-residual-a-camera-collision-design.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two Core physics primitives retail's SmartBox::update_viewer calls down into,
ported verbatim (TDD, 7 new tests):
- CellTransit.FindVisibleChildCell (CEnvCell::find_visible_child_cell, pc:311397):
return the cell whose cell-BSP point_in_cell contains a world point — start cell
first, then (stab-list mode) the start's VisibleCellIds or (portal mode) its
direct portals. Sibling of FindCellList. Mirrors FindCellList's null-CellBSP skip
(CellTransit.cs:518) so a cell lacking hydrated CellBSP doesn't spuriously claim
every point via PointInsideCellBsp's null-node "inside" default.
- PhysicsEngine.AdjustPosition (CPhysicsObj::AdjustPosition, pc:280009): resolve a
point's cell from a seed. Indoor (>=0x100) → FindVisibleChildCell(stab-list);
outdoor → landcell snap (same grid lookup as ResolveCellId). The seen_outside
sub-fallback is deferred (off the cottage/cellar path; spec §6).
Both are unwired into any production path — they land the machinery update_viewer's
start-cell + fallback 1 need (and that residual C also needs). The App SweepEye
orchestration that calls them lands next.
Decomp-faithful per the live-capture finding: A's V1 sweep already contains the eye
(eyeInRoot=Y 99.75%, never void); this completes A as a verbatim port. Spec:
docs/superpowers/specs/2026-06-05-residual-a-camera-collision-design.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stale-footCenter fix (cc4590f) is visually confirmed: cellar ascent is
smooth, inn door still blocks, generic step-up still climbs. The residual
9/29 (0,-1,0)-sliding-normal records did NOT manifest in live play —
confirming they were buggy-trajectory artifacts.
Remove the temporary investigation scaffolding added for this trace:
- [fc-dispatch] probe in BSPQuery.FindCollisions
- [step-sphere-down] probe in BSPQuery.StepSphereDown
- CellarLipWedgeTests.Diagnostic_TraceRecordByIndex [Theory]
Kept: the fix, the Fix_StaleFootCenter_* regression guards, and the
DocumentsResidualWedge_* documents-the-bug test. Core suite 1317 pass /
4 fail (documented baseline) / 1 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the "blocked at the last cellar step" wedge (the primary,
ramp-climb family — 20/29 captured records). The prior session's pinned
"find_walkable is never called during the step-down" was a probe artifact:
a fresh [fc-dispatch]/[step-sphere-down] trace proves Path-3 StepSphereDown
IS reached for both the carried cell and the iterated other-cell.
The real divergence is in Transition.CheckOtherCells. Retail's
check_other_cells (acclient_2013_pseudo_c.txt:272735 → (*cell+0x88)(this))
re-collides the OTHER cells against the LIVE sphere_path.global_sphere — the
position AFTER the primary insert_into_cell ran. The primary collide can MOVE
the sphere: a Path-5 full-hit dispatches step_sphere_up, and a successful
step-up climbs the foot onto the cottage floor yet still returns OK. acdream
instead reused a footCenter snapshot captured BEFORE the primary collide, so
once the lip-riser step-up climbed the foot onto the floor, check_other_cells
still queried 0171 at the pre-climb (sunk ~0.25 m below the floor) position →
the foot spuriously near-missed the very floor it had climbed onto →
neg_step_up → a doomed second step_up vs the floor normal (0,0,1) whose
step_up_slide unwound the climb → validate_transition reverted → 0% advance.
Fix: re-read footCenter = sp.GlobalSphere[0].Origin at the top of
RunCheckOtherCellsAndAdvance (one line). Pre-fix 0/29 wedge records advanced;
post-fix 20/29 climb onto Z≈94.
No regression: full Core suite 1321 pass / 4 fail (the documented baseline:
Apparatus_Grounded_50cmOffCenter, 2× DoorBugTrajectoryReplay LiveCompare_*,
BSPStepUpTests.D4) / 1 skip. The 2 door LiveCompare divergences are
byte-identical with/without the fix (the door's step_up FAILS → sphere
restored → position unchanged → footCenter == live).
Tests: CellarLipWedgeTests.Fix_StaleFootCenter_RampRecordClimbsCottageFloor +
Fix_StaleFootCenter_MajorityOfWedgeRecordsAdvance (new, GREEN).
DocumentsResidualWedge_LiveFloorCp_SlidingNormalKillsPlusY documents the
remaining 9/29 (0,-1,0)-sliding-normal +Y-kill family (slide territory,
deferred to the visual gate).
Apparatus retained (gated on ACDREAM_PROBE_INDOOR_BSP): [fc-dispatch] in
BSPQuery.FindCollisions + [step-sphere-down] in BSPQuery.StepSphereDown +
CellarLipWedgeTests.Diagnostic_TraceRecordByIndex — strip once the residual
is resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P2 / M1.5 "blocked at the last step" cellar-lip wedge. This session built a faithful
deterministic reproduction and peeled the cause through six evidence-disproven framings
to one bounded question. NO fix landed — the last layers were each disproven by evidence,
and guessing at the load-bearing collision code is the saga's failure mode.
Apparatus:
- CellarLipWedgeTests.cs + Fixtures/cellar-lip/ (3 real cell dumps + wedge-records.jsonl =
29 captured ACDREAM_CAPTURE_RESOLVE wedge calls). Replays the exact calls + body-before
through the lip-cell engine: all 29 reproduce at 0% advance in <200 ms. Tests are
documents-the-bug / diagnostics (GREEN while the wedge exists).
- TEMP probes ([path5-wall]/[fw-enter]/[find-walkable] in BSPQuery; [neg-poly]/[stepsphereup]/
[stepdown-decide]/CheckOtherCells cn/sn/negHit in TransitionTypes), gated on
ACDREAM_PROBE_INDOOR_BSP, marked STRIP. TransitionTypes neg-poly shortcut has a reverted-fix
comment (slide attempt didn't clear the wedge).
- tools/cdb/retail-*-trace.cdb (retail cdb traces).
Findings (handoff: docs/research/2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md, see the
"NEXT-SESSION KICKOFF" at top):
- Flat-floor contact plane is retail-faithful (v1 trace, full-file correlation). NOT the bug.
- PosHitsSphere cull sign is retail-faithful (cdb -z verified; the Binary Ninja `test ah,N; jp`
parity-jump reads inverted — caught + reverted a wrong fix from that mis-read).
- Sphere radius correct (0.48 player / 0.30 camera probe).
- Retail connector cell 0xA9B40175 never blocks (CEnvCell::find_collisions trace: 0 Collided/Slid).
- PINNED: during the step-up's step-down, BSPQuery.FindWalkableInternal is never called for cell
0171, so the cottage floor (poly 0x0023, Z=94) is never tested as walkable -> no contact plane
-> step-up fails -> StepUpSlide=Collided -> wedge.
Next: trace FindEnvCollisions -> FindCollisions path dispatch for 0171 during StepDown=true (why
StepSphereDown/find_walkable is skipped), port retail, validate via CellarLipWedgeTests, regress
DoorBugTrajectoryReplayTests + visual gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the A6.P4 Collided shortcut in transitional_insert's neg_step_up==0
branch with the faithful CSphere::slide_sphere. Retail
(acclient_2013_pseudo_c.txt:273350-273351, CTransition::transitional_insert):
neg_step_up==0 (HEAD-sphere near-miss) -> slide_sphere -> continue the insert
loop; neg_step_up==1 (foot) -> step_up_slide. The acdream foot branch already
did that; only the head branch took the shortcut (SetCollisionNormal + return
Collided = dead hard-stop). The slide itself is the existing
SlideSphereInternal (Sphere.SlideSphere port): it strips the into-wall
component and keeps the tangential crease (collisionNormal x contactPlane.N).
Surfaced by the B1 near-miss-gate fix (abbd761): once the grounded mover climbs
onto the cottage floor, its head sphere brushes the cellar stairwell walls and
the old hard-stop wedged it (2026-06-04 live capture: 274 (0,-1,0) + 78
(1,0,0) hits, out==current, dead oscillation). Post-fix capture shows 96
hit-and-advanced frames (the body slides along the walls).
Visual-verified 2026-06-04: closed cottage door still BLOCKS (no walkthrough --
drifts sideways along it, retail-faithful); cellar ascent now works (was always
stuck). An intermittent corner-wedge (slide into the -Y/+X wall corner) remains
-- separate finer issue, under investigation.
Core 1310 pass / 4 fail (pre-existing: 3 door documents-the-bug + D4 airborne).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2026-06-03 handoff localized the failing Core tests to the BSP Path 5
step-up CLIMB (find_walkable/step_sphere_down). An ITestOutputHelper capture
of B1 disproved that: the climb code is correct (matches ACE
Polygon.adjust_sphere_to_plane / BSPTree.step_sphere_down exactly). The real
bug is the A6.P4 near-miss dispatch in FindCollisions' Path 5 (Contact
branch), which diverged from retail three ways:
1. Recorded a near-miss NegPolyHit UNCONDITIONALLY. Retail gates both
set_neg_poly_hit calls behind `if (num_sphere > 1)`
(acclient_2013_pseudo_c.txt:323852).
2. Checked the foot sphere's near-miss before the head's. Retail checks
the head (sphere1) first.
3. Mapped foot->neg_step_up=false / head->true. Retail maps head(index 0)
->false (slide), foot(index 1)->true (step-up), per
SPHEREPATH::set_neg_poly_hit (:323279, neg_step_up = arg2).
For B1's single foot sphere, the spurious near-miss -> outer loop
`!NegStepUp -> SetCollisionNormal + Collided` -> revert: the grounded mover
wedged at x=0.1 and never advanced to the wall to step up. With the verbatim
gate, a single-sphere near-miss records nothing, the sphere advances,
full-hits the wall, and step_sphere_up climbs the 0.25 m step (verified via
probe capture: foot ends at (0.6, 0, 0.25)).
The Holtburg cottage door still blocks faithfully (door slab (0,-1,0) normal,
stops in front of the door) when the scenario has a real floor — confirmed
this change does not regress the door.
The two BSPQueryTests Path5 near-miss tests used a single sphere (the very
non-retail assumption that caused this wedge); converted to the production
2-sphere shape where the head sphere records the near-miss, matching retail.
Core 1312 pass / 4 fail (the 4 pre-existing: 3 door documents-the-bug + D4
airborne, none regressed here); App 177 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
THE doorway flap root cause, found via [flap-cam]/[shell]/[cell-transit] (2026-06-03):
the player spawned + stood still in the room (cell 0171, NO [cell-transit] after teleport),
yet the render rooted at the vestibule (0170) for all 77,951 frames — drawing only 0170's
~8-triangle shell, the rest = GL clear color = the bluish void.
CellGraph.CurrCell IS "the player's cell" (the render root), but it was written by
SetCurrAndReturn inside the PER-ENTITY ResolveWithTransition + ResolveCellId — so EVERY NPC
wrote it. A Holtburg NPC (0x000F4240) jump-looping near the doorway clobbered the player's
render root every tick. Standing still (player makes no resolve calls) the NPC's write wins
→ stuck blue void; moving, player/NPC writes fight → the flap. This is why the membership
pick fix (correct, kept) didn't change the visual — the render root was clobbered regardless.
Fix: CurrCell is now written ONLY by the player. New PhysicsEngine.UpdatePlayerCurrCell is
called from PlayerMovementController.UpdateCellId — the single player-only chokepoint for
CellId (teleport / server snap @ SetPosition + per-frame resolver). Removed the CurrCell
write from SetCurrAndReturn (inlined the 2 resolve call sites to sp.CurCellId) and the 4
ResolveCellId sites. NPCs no longer touch the render root. Teleport→UpdateCellId also covers
spawn/standing-still (CurrCell = the player's spawn cell immediately).
CellGraphMembershipTests rewritten to the new contract (3 tests): UpdatePlayerCurrCell writes
the render root; ResolveCellId does NOT (the blue-hole guard); stale-beats-null preserved.
Full Core suite: 1295 pass / 5 fail = the documented §10 baseline, zero new breakage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ordered pick alone did not stop the cottage doorway flap: the live [cell-transit]
log showed the cell faithfully following a position that cleanly oscillated between two
values at constant Z — the signature of a bistable membership<->collision feedback loop
(user's §4.4 #3, the forked collision).
Root cause: FindEnvCollisions RE-PICKED the cell from the TARGET position (old line 1958)
BEFORE running the primary collision, so the collision geometry (which cell BSP / terrain)
swapped the instant the pick flipped -> position shifts -> pick flips back.
Retail does NOT do this. CEnvCell::find_env_collisions (acclient_2013_pseudo_c.txt:309573)
collides against the cell it was called ON (sphere_path.check_cell, the carried seed) and
picks the NEW containing cell AFTER, in CTransition::check_other_cells (272717->272761:
check_cell = var_4c). Collide-then-pick.
This commit ports that order:
- remove the pre-pick (production); collide against the carried cell (indoor BSP block /
terrain block unchanged);
- new shared RunCheckOtherCellsAndAdvance() runs the ordered FindCellSet pick +
multi-valued CheckOtherCells + the carried-cell advance AFTER the primary collision, for
BOTH indoor and outdoor seeds;
- the outdoor-seed post-step replaces the removed pre-pick's outdoor->indoor re-entry
promotion (CheckBuildingTransit interior cell + its wall collision on the entry frame).
Cache-null unit-test fallback (ResolveCellId) kept. Full Core suite: 1293 pass / 5 fail =
the documented §10 baseline exactly (2 step-up + 3 door-collision), zero new breakage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port CObjCell::find_cell_list (acclient_2013_pseudo_c.txt:308742) faithfully:
- build candidates into an ordered CellArray with the CURRENT cell at index 0
(add_cell @308766);
- EXPAND via a single forward walk over the growing array, mirroring retail's
for(i=0;i<num_cells;i++) cells[i].find_transit_cells loop (308775-308785),
replacing the order-losing Queue/visited BFS;
- PICK in array order with interior-wins-break (308788-308825): current cell at
index 0 wins a boundary straddle, so membership no longer ping-pongs.
Deletes the 5ca2f44 current-first pre-check (the ordered array subsumes it for every
seed). Keeps its guard test (TwoOverlappingCells_CurrentCellWinsTheStraddle) + adds
two conformance tests (current-cell-first ordering; interior-wins over outdoor
fallback). Membership net: 45 pass. Decomp finding: retail stability is emergent from
the ordered pick + carried seed, not a separate portal-crossing detector — see
docs/research/2026-06-03-cell-membership-ordered-cellarray-pseudocode.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-behavioral: lets BuildCellSetAndPickContaining pass an ordered CellArray (next
commit) while existing HashSet-passing test callers compile unchanged. HashSet<uint>
and CellArray both implement ICollection<uint>. Core builds; 9 helper tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports retail CELLARRAY::add_cell (acclient_2013_pseudo_c.txt:701036): ordered list,
dedup by cell_id, append at end. The order is load-bearing for the verbatim
find_cell_list current-cell-first interior-wins pick (next commits) that fixes the
R1 cottage membership flap. Implements ICollection<uint> (helper-facing) +
IReadOnlyCollection<uint> (consumer-facing). 5 unit tests.
Also lands the membership-port pseudocode (workflow step 3) + the Stage-1 plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flap R1 exposed is a cell-membership ping-pong: the find_cell_list containing-
cell pick (CellTransit.BuildCellSetAndPickContaining) iterated an UNORDERED HashSet
and returned the first interior cell whose BSP contains the sphere center, with no
preference for the current cell. Retail CObjCell::find_cell_list adds the current
cell at index 0 (add_cell, pc:308766) and iterates current-first with interior-wins-
break (pc:308791-308819) — you STAY in your current cell until the center genuinely
leaves it. acdream's HashSet dropped that ordering; once the candidate set churns at
a boundary the enumeration can surface a neighbour before the current cell → the
ping-pong. Restore the explicit, deterministic current-cell-first test (retail's
index-0 hysteresis). + a two-direction regression guard (current cell wins the
straddle).
Diagnosed from the existing [cell-transit] walk log (no new probing): room flips are
the pick non-determinism; stairs flips additionally show the foot Z oscillating
~0.2m/tick (a separate stairs-physics residual, #98 family, to verify after this).
The 2 DoorBugTrajectoryReplay failures are PRE-EXISTING (verified: they fail without
this change too) — 2 of the handoff's '3 door-collision apparatus / A6.P5'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port CEnvCell::find_visible_child_cell @ 0x0052dc50 (pseudo_c:311397): walk
rootId's StabList + the root itself, return the first EnvCell whose
PointInCell is true for worldPoint. Used to resolve the camera cell in
3rd-person from the physics cell graph rather than a fresh AABB reclassification.
Root is always the player cell (preserves U.4c flap fix). Returns null when
no stab-list cell or root itself contains the query point.
Confirmed (T3.3 Step 1): no production call site uses FindCameraCell for the
camera projection — the only AABB camera resolver is now deleted (T3.1).
FindVisibleChildCell is wired implicitly via Stage 4 (camera-outside-door scenario);
no GameWindow call site needed in Stage 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xUnit's default parallel execution let diagnostic-harness tests (CellarUp,
DoorBug, DoorCollisionApparatus) mutate PhysicsResolveCapture.CapturePath
and PhysicsDiagnostics probe flags concurrently with victim tests
(MotionInterpreter, PositionManager, PlayerMovementController,
DispatcherToMovement, BSPStepUp), producing a flaky 14-26 failure range.
Fixes:
- Add PhysicsResolveCapture.ResetForTest() + PhysicsDiagnostics.ResetForTest()
as documented test-only reset APIs (never called from production paths).
- Add IDisposable to CellarUpTrajectoryReplayTests with ctor/Dispose calling
both ResetForTest() — prevents CapturePath from leaking between the Capture_*
tests in the same class (the immediate root cause of Capture_SkipsNonPlayerCalls
finding an unexpected file).
- Add xunit.runner.json (maxParallelThreads=1, parallelizeTestCollections=false)
to AcDream.Core.Tests — eliminates parallelism-induced probe-flag leaks across
all test classes without requiring [Collection] boilerplate on every offender.
After: two consecutive runs produce the identical 12-failure set.
Confirmed: LiveCompare_FirstCap_FixClosesCottageFloorCap passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change A (TransitionTypes.FindEnvCollisions:~1947): replace the unconditional
static ResolveCellId re-derive with the SWEPT find_cell_list pick via
CellTransit.FindCellSet. When DataCache is available (always in production),
the swept pick runs and resolves the containing cell from the portal-graph
candidate set. When DataCache is null (test engines without a cell registry),
the old ResolveCellId fallback is preserved to keep PhysicsEngineTests green.
Change B (CellTransit.BuildCellSetAndPickContaining): replace the containment
loop that silently skipped all outdoor candidates (CellBSP=null) with the
retail CObjCell::find_cell_list interior-wins pick (pseudo_c:308788-308819):
interior EnvCells win first; if no interior cell contains the center, fall
to the outdoor XY-grid column (CLandCell::point_in_cell equivalent). This is
the missing half of find_cell_list that caused the 0xA9B40170↔0xA9B40031
doorway cell-strobe — the swept pick previously always returned currentCellId
for outdoor candidates, letting the static re-derive at :1947 strobe on every
tick from a different result.
DoorwayMembershipReplayTests: two facts, loads doorway-capture.jsonl (364K records,
strobing live run), filters to Y∈[15.5,17.5] seam zone (57 records), verifies
FindCellSet produces exactly 1 transition (enter indoor → stay outdoors) with
zero A→B→A ping-pong across the full window. Second test verifies outdoor-seed
records round-trip correctly via the XY-grid formula. Both pass.
LiveCompare_FirstCap_FixClosesCottageFloorCap: still passes (issue #98 gate intact).
Full Core suite: 15 failures (within documented flaky baseline of 14–19;
all 15 are pre-existing static-leak/document-the-bug tests, zero new regressions
in cell/transit/BSP/physics classes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace ResolveCellId(sp.GlobalSphere[0].Origin, ...) with SetCurrAndReturn(sp.CurCellId)
in both the OK and partial paths of ResolveWithTransition. Retail's
SetPositionInternal reads sphere_path.curr_cell which ValidateTransition
advances only on accepted moves and reverts on blocks — so a push-back or
standing-still tick cannot flip the cell. The static re-derive from the
resting origin strobes between outdoor 0031 and indoor 0170 at doorway
boundaries because the origin lands just outside the indoor BSP volume
after push-back; the swept cell doesn't.
SetCurrAndReturn is kept in both paths so the W2a CellGraph.CurrCell write
that the render root consumes still fires. ResolveCellId is NOT deleted —
it still has one caller at TransitionTypes.cs:1947 (AddAllOutsideCells).
partialCellId is kept as the degenerate fallback when sp.CurCellId==0
(teleport / physics reset before any transition has run).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ProbeSweptEnabled (ACDREAM_PROBE_SWEPT=1) to PhysicsDiagnostics mirroring
ProbeCellEnabled. Emits one [cell-swept] line per ResolveWithTransition call —
sp.CurCellId and sp.CheckCellId (the transition's swept cells) alongside the
incoming cellId so a doorway capture shows whether the swept cell is stable
where ResolveCellId strobes. No ResolveCellId call in the probe — avoids the
CellGraph.CurrCell side effect. No behavior change.
TDD: ProbeSweptEnabled_DefaultsToFalse RED→GREEN in PhysicsDiagnosticsTests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port retail CObjCell::find_cell_list do_not_load_cells prune
(acclient_2013_pseudo_c.txt:308829-308867) as indoor->outdoor doorway
hysteresis: hold the previous indoor cell when the outdoor candidate is
not in its stab list AND the foot-sphere still overlaps the cell's
containment BSP expanded by DoorwayHoldMargin. Kills the front-door
0170<->0031 ping-pong (handoff §5) the #98 saga never addressed. Fires
only at the front-door seam; the cellar has no exit portal so it never
falls through here (#98 cellar-up untouched).
Three TDD tests in CellGraphMembershipTests: HOLD (the RED->GREEN case,
Y=3.9 inside the 0.2 m margin), RELEASE when fully outside (Y=4.5
exceeds expanded margin), and stab-list gate (outdoor candidate in stab
list releases even near the boundary).
Adds using System.Linq for IReadOnlyList.Contains at the prune site.
SphereOverlapsEnvCell helper mirrors BSPQuery.SphereIntersectsCellBsp
via EnvCell.InverseWorldTransform + ContainmentBsp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add private `SetCurrAndReturn(uint)` helper in PhysicsEngine that looks up
the resolved id in `DataCache.CellGraph` and writes `CurrCell` when the cell
is present. Wrap the four RESOLVED-id return sites in ResolveCellId:
- indoor no-CellBSP return (trust FindCellList)
- indoor sphere-overlaps-CellBSP return
- outdoor→indoor building-transit return (foreach candidate)
- outdoor terrain-grid return
The final no-match `return fallbackCellId;` is intentionally NOT wrapped —
stale beats null (the caller's seed is preserved unchanged).
CurrCell has zero readers in src/ (verified by ripgrep); this is additive
write-only, identical observable behavior to W1. One new unit test
(CellGraphMembershipTests) proves RED→GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PhysicsDataCache gains a `CellGraph` property (UCG Stage 1). The env-cell
hook is placed at the very top of CacheCellStruct — before the idempotency
guard and the null-PhysicsBSP early-return — so BSP-less cells are included
in the graph even though they are dropped from the legacy _cellStruct map.
PhysicsEngine.AddLandblock/RemoveLandblock mirror terrain registration into
the graph via a null-guarded DataCache?.CellGraph call. Zero behavior change:
CellGraph has no readers this stage.
A using-alias (UcgEnvCell / UcgCellGraph) resolves the EnvCell name
collision between AcDream.Core.World.Cells and DatReaderWriter.DBObjs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Outdoor terrain cell (retail CLandCell) synthesized on demand from a
landblock's TerrainSurface. Factory Synthesize() samples four quad
corners to establish Z bounds; PointInCell() tests the 24 m XY quad
in world-local space. BuildingCellId stub is null (Stage 2).
2/2 tests RED→GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `EnvCell` (sealed, extends `ObjCell`) with a primitive constructor
and `PointInCell` that uses the cell-containment BSP when present, else
falls back to an AABB test. Retail anchor: CEnvCell (acclient.h:32072).
BSP branch delegates to `BSPQuery.PointInsideCellBsp` (BSPQuery.cs:1034);
the AABB branch is the genuinely new logic. No `FromDat` factory — that is
a separate later task. Consumed by nobody yet (Stage 1 scaffold).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces AcDream.Core.World.Cells namespace with the two foundational
types for the Unified Cell Graph. CellPortal is a readonly struct
unifying the three legacy portal representations; ObjCell is the abstract
base for all traversable cells with the retail id-magnitude IsEnv
discriminator (CObjCell::GetVisible, pseudo_c:308215). Zero consumers;
zero behavior change. 5/5 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A week on the indoor render (Phase U.4 → U.4c → 2026-05-31) fixed the flap but
produced NO shippable progress: walls/ceiling don't seal, outdoor terrain is
visible from inside (#78), the enclosure reads grey/transparent. Root cause is
ARCHITECTURAL, not a bug.
Evidence this session (direct, via the new [shell] probe + screenshots) RULED OUT
every subsystem except the gating architecture: the interior cell shells render
fine (geometry/texture/opaque/depth all correct, zh=0 tr=0); the visibility
traversal computes correct sets + non-empty portal clips; cull mode is fine; the
camera/eye thread was a detour. The residual is that OUTDOOR geometry is not gated
to portal openings when indoors, and acdream enforces visibility THREE inconsistent
ways (TerrainClipMode / per-cell shell clip / entity ParentCellId filter with an
outdoor-stab bypass) instead of retail's ONE PView gate.
This commit is the reset handoff + documentation, not a code fix:
- docs/research/2026-05-31-render-architecture-reset-handoff.md — canonical: honest
state, evidence ledger (ruled-out / do-not-repeat), the mapped 3-gate patchwork,
the retail PView target (one traversal → one gate for ALL geometry), the reset
mission, and a copy-paste pickup prompt.
- docs/architecture/acdream-architecture.md — new "Render Pipeline" SSOT section
(current divergence + unified-PView target + the one rule: compute visibility
once, enforce it once). (Doc has pre-existing corruption below this section —
flagged for separate cleanup.)
- Apparatus: ACDREAM_PROBE_SHELL → [shell] (EnvCellRenderer per-cell prepared/drawn
geometry + flags) added to RenderingDiagnostics + EnvCellRenderer. Throwaway.
- docs/superpowers/specs/2026-05-31-camera-collision-indoor-engagement-design.md —
spec for e099b4c (camera collision; now parked as orthogonal to the seam).
Next session: STOP point-fixing; do the architecture reset to a single PView gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause: ShadowObjectRegistry.GetNearbyObjects gated the outdoor radial sweep
whenever primaryCellId is an indoor cell — this was the issue-#98 fix that stops the
cottage-floor GfxObj from capping the player's head sphere from the cellar below.
But the camera probe (ObjectInfoState.IsViewer, 0x004) also sweeps with an indoor
primary cell, and the only geometry that encloses a Holtburg cottage in acdream's data
model is the landblock-baked exterior-shell GfxObj (registered cellScope=0, outdoor).
Result: the camera's spring-arm sweep found nothing and flew to full chase distance
(eye ~3.4 m back, outside the player's cell 90% of frames — root cause of all three
post-flap residuals: transparent outer walls, terrain-through-floor, grey stairs).
Fix (Option A, retail-faithful): add isViewer parameter (default false, all existing
callers keep the gate) to GetNearbyObjects. Thread oi.IsViewer from FindObjCollisions
(TransitionTypes.cs ~line 2307) through to the gate. When isViewer=true the outdoor
sweep runs regardless of indoor primary cell — matching retail's SmartBox::update_viewer
(:92761) which calls find_obj_collisions (:308918) with no indoor-cell restriction.
The #98 gate remains in force for IsPlayer and all other non-viewer sweeps.
Retail anchors:
- SmartBox::update_viewer @ acclient_2013_pseudo_c.txt:92761 — viewer transition
finds geometry via find_obj_collisions; no indoor gate
- find_obj_collisions @ :308918 — iterates shadow_object_list unconditionally
- CObjCell::find_cell_list @ :308751-308769 — retail's own indoor/outdoor branch
(the model that makes the #98 gate correct for the player)
Also fixes a test-fixture geometry bug: the original RED test had
gfxLeaf.BoundingSphere.Origin in world space (0, ExteriorWallY, 96) instead of
object-local space (0, 0, 0), causing NodeIntersects to return false even when the
gate was bypassed. Corrected to local space; wall polygon vertices/plane also
expressed in local space relative to the GfxObj origin.
Tests (3 new, 1 renamed):
- SweepEye_IndoorCellExteriorGfxObjWall_StoppedByExteriorShell_AfterViewerGateExemption:
was RED (_CurrentlyFails); now GREEN — camera sweep stopped by exterior GfxObj wall
- GetNearbyObjects_IndoorPrimaryCell_NonViewer_DoesNotReturnOutdoorGfxObj: #98 guard
(isViewer=false keeps the gate → GfxObj NOT returned)
- GetNearbyObjects_IndoorPrimaryCell_IsViewer_DoesReturnOutdoorGfxObj: viewer-exempt
guard (isViewer=true bypasses gate → GfxObj IS returned)
App.Tests: 154 pass / 0 fail (was 151/1). Core.Tests: 15 fail (same pre-existing
static-leak flakiness, unchanged from baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-frame (not cell-change-throttled, so it catches the flicker at a stable root):
[flap] line from the builder — root cell's per-portal side-test D + traverse/cull +
NDC projection, plus OutsideView poly count + visible-cell count; localEye exposes
when the eye has crossed an interior portal plane. Paired [flap-cam] line from the
draw site — FindCameraCell resolution branch (CameraCellResolution enum, new),
eyeInRoot AABB flag (stale-root signal), eye + player worldpos, and the frame's
TerrainMode/OutdoorVisible outcome. Disambiguates side-cull vs empty-projection vs
stale-root. Inert when off (gated). Throwaway apparatus to converge the flap fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the durable per-frame visibility probe apparatus that #103 lacked, so
the Phase U portal-visibility builder can be validated on live frames before
any GL/visual work.
EmitVis(rootCellId, visibleCells, outsidePolyCount, outsidePlaneCount,
perCellPlaneCounts, scissorFallbacks) prints ONE concise [vis] line gated on
root-cell CHANGE (private _lastVisRootCellId tracker; no-op when the root is
unchanged or ProbeVisibilityEnabled is false — one bool compare per frame when
off). Line format:
[vis] root=0x… cells=N ids=[…] outside(polys=…,planes=…) percell=[0x…:N,…] fallbacks=…
Reuses the existing Phase A8 ProbeVisibilityEnabled flag (env ACDREAM_PROBE_VIS,
already DebugPanel-mirrored via DebugVM.ProbeVisibility) rather than adding a
parallel owner — Code Structure Rule 5 (one diagnostic owner per subsystem).
Property doc repurposed from the abandoned A8 two-pipe stencil semantics to the
Phase U unified pipeline.
Decoupling note: RenderingDiagnostics lives in AcDream.Core, which must not
reference AcDream.App (Code Structure Rule 2). The plan's EmitVis signature took
an App-layer CellView; this lands the equivalent as pre-computed primitives
(outsidePolyCount + outsidePlaneCount) so the owner stays in Core. The U.4a call
site supplies OutsideView.Polygons.Count and the OutsideView ClipPlaneSet.Count.
TDD: 3 new tests in RenderingDiagnosticsVisibilityTests (no-op when disabled,
fires-once-per-new-root + suppressed-on-unchanged, env-default contract), each
self-contained via internal ResetVisibilityProbeForTests + Console.Out capture
to avoid the documented static-leak flakiness. Core suite +3 tests, no new
failures (flaky physics/input static-leak set unchanged at 16, untouched area).
Courtesy: removed the dangling RenderInsideOutAcdream comment reference (deleted
in U.1) + the AcDream.App.Rendering.Wb doc cref (a Core→App layer inversion).
The emit SITE wiring (per-frame call from the render loop) lands in U.4a; this
task lands only the owner members + formatter + test. GameWindow untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail's CTransition::find_transitional_position (:273613) has no step
cap. calc_num_steps (:272149) has a dedicated viewer branch `if ((state
& 4) != 0)` at :272181 for sight/viewer objects (ObjectInfoState.IsViewer
= 0x4). The existing acdream cap correctly had a comment "Sight objects
bypass this" but the bypass was never wired — no IsViewer caller existed
until the A8.F camera spring-arm.
With radius 0.3 m the cap fires at ~9 m. The spring-arm sweeps up to
40 m (≈134 steps), so zoomed-out cameras snapped to the player's head
instead of sweeping through geometry. The fix adds `&& !ObjectInfo.IsViewer`
to the guard; non-viewers keep the 30-step safety net (player spheres
~0.48 m radius never exceed 14 m/tick).
Conformance test: radius=0.3, dist=12 (40 steps > 30 cap) over flat
terrain. Normal mover bails (Assert.False). Viewer proceeds to target
(Assert.True + CurPos.X > from.X). RED → GREEN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>