Verbatim port of CObjCell::find_cell_list (Ghidra 0x0052b4e0, pc:308742)
as invoked by calc_cross_cells / calc_cross_cells_static (0x00515230 /
0x00515160) - the flood retail runs at SHADOW REGISTRATION time, minus
the containing-cell pick (registration passes a null out-cell):
- Seed: indoor -> exactly that one cell (added even when unloaded, like
retail's null-pointer add_cell; the walk is then skipped per the
0052b576 seed-pointer gate); outdoor -> block-crossing
AddAllOutsideCells.
- Growing-array walk: indoor cells via FindTransitCellsSphere
(sphere-vs-neighbor-BSP admission; exterior straddle -> outside cells
once per walk, retail CELLARRAY.added_outside); outdoor cells via the
CLandCell leg (0x00533800) = add_all_outside_cells (same once-guard)
+ the building bridge CSortCell -> CBuildingObj ->
check_building_transit (0x00534060/0x006b5230/0x0052c5d0) - how an
outdoor-positioned door reaches the vestibule's shadow list at
registration. No XY grid, no visibility lists (the spec's
VisibleCellIds rule was REFUTED by the WF1 verification).
- Static prune (do_not_load_cells, 0052b66e): indoor-seeded statics keep
only {seed} + seed.stab_list (VisibleCellIds) - also strips outdoor
cells, matching retail (interior statics never shadow into landcells;
outdoor spheres reach them through their own array's building bridge).
11 unit tests: seeds (indoor/outdoor/unloaded), neighbor-BSP admission,
building bridge incl. the C1 negative-portal-id gate, exterior straddle
on/off, static prune drop/keep, zero-sphere no-op.
Consumed by C3 (ShadowObjectRegistry rewrite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail CEnvCell::check_building_transit (Ghidra 0x0052c5d0) opens with
`if (other_portal_id >= 0)` on the SIGNED sign-extended portal id
(CBldPortal.other_portal_id is int, acclient.h:32098). Our BldPortalInfo
carried the dat reader's raw ushort and CheckBuildingTransit had no gate
at all, so a portal whose dat value is 0xFFFF (-1, "no reciprocal
portal") could admit its interior cell. BN's pseudo-C renders the
comparison unsigned - the sign-extension is Ghidra-proven (BR-7 verified
corrections, wf1-interior-collision.md).
- BldPortalInfo.OtherPortalId: ushort -> short; GameWindow construction
reinterprets the dat ushort via unchecked((short)).
- CheckBuildingTransit: negative-id portals rejected before any sphere
test; new multi-sphere overload matching retail's per-sphere loop
(0052c5fe, first-hit admits) with the hits_interior_cell output
(0052c650) the BR-7 building channel consumes next.
- Tests: negative-id skip vs positive-id admit on a leaf-root CellBSP;
multi-sphere plumbing + zero-sphere no-op.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The oracle read the #112 residual was waiting on, settled against the
LIVE 2013 client (cdb attach, CEnvCell::find_transit_cells @ 0052c820;
BN pseudo-C was ambiguous and partly wrong per
feedback_bn_decomp_field_names - it invented portal_side tests in this
branch): retail admits outdoor transit cells from an indoor cell IFF a
path sphere STRADDLES an exterior portal polygon plane,
|dist| < radius + F_EPSILON(0.000199999995, @ 007c8c70). The flag at
[esp+18h] (set 0052c925, x87 decode fcompp/test ah,41h +
fcomp/test ah,5/jp) gates the add_all_outside_cells call (0052c9d6 je).
Graph reachability alone NEVER admits outdoor cells in retail.
Port (CellTransit):
- FindTransitCellsSphere: exitOutside now carries the retail straddle
semantics; new hasExitPortal out carries the old topology-only flag.
- BuildCellSetAndPickContaining: the collision cell SET keeps the A6.P5
topology widening on hasExitPortal (outdoor-registered doors must stay
findable from indoor cells until #99/A6.P4 ships per-cell shadow
lists - the 2026-05-25 door capture scenario), but the membership
PICK's outdoor branch is gated on the retail flag. Membership is now
retail-identical in both regimes: straddle -> outdoor candidates valid;
no straddle -> outdoor ignored -> retail keep-curr. This is what stops
deep-interior containment gaps in ANY house from demoting to outdoor
(the #112 transparent-interior shape) - the systemic protection the
user asked for, without house-by-house verification.
The at-doorway A9B3 gap demote is RETAIL-FAITHFUL (gap point is 0.23m
from 0x104s door plane < 0.48 foot radius -> retail straddles + demotes
+ self-heals inward): DocumentsResidual renamed to
...DemotesRetailFaithfully, expectation unchanged. New conformance pins:
deep-gap keep-curr (A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell)
+ function-level gate semantics on real dat geometry
(FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail).
Tests: Core 1391 green (+2) / App 224 / UI 420 / Net 294; pre-existing
4 #99-era failures unchanged; P1 membership goldens + A6.P5 door-set
tests explicitly green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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 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>
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>
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>
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>
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>
Retail's CObjCell::find_cell_list at acclient_2013_pseudo_c.txt:308742-
308869 walks vtable[0x80] on every cell in the array and adds portal-
reachable cells unconditionally — without testing each portal plane
against the sphere. Our exit-portal branch in FindTransitCellsSphere
gated outdoor inclusion on sphere-plane overlap (exitOutside fired
only when the sphere physically straddled the exit portal plane).
That gate produced the cottage-door over-penetration bug verified in
A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell: BFS from
indoor cell 0xA9B4013F expanded to 0xA9B40150 (which has an exit
portal) but the sphere — in 0xA9B4013F's volume — wasn't at 0xA9B40150's
exit portal plane, so exitOutside stayed false and the door's outdoor
cell 0xA9B40029 wasn't added to the cellSet. The cell-crossing tick's
collision query missed the door and the sphere committed 0.27 m INTO
the slab.
Fix: exit portals contribute exitOutside=true by topology
(OtherCellId == 0xFFFFu), not by sphere overlap. AddAllOutsideCells
is deduped to once per BFS so the radial pattern is added exactly
once when any BFS-visited cell has an exit portal.
Conformance: A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell
now passes. A6P5_BuildCellSetFromAlcove_AlsoReachesDoorOutdoorCell
(regression guard for the previously-sometimes-working case) stays
green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One [cellset-build] line per call when ACDREAM_PROBE_CELLSET=1: seed cell,
sphere world XY, candidate count, full candidate id list. Used to prove
the cellSet for the player's start cell doesn't include the door's outdoor
cell across the over-penetration tick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CellTransit.AddAllOutsideCells assumed sphere coords were absolute world
coords (subtracting lbXf = 0xA9 * 192 = 32448 from the sphere position).
Production has used landblock-local coords since Phase A.1
(streaming-center landblock at world origin), so the subtraction
produced localX = -32316, gridX = -1346 → out-of-range → early return
→ ZERO outdoor cells added.
For outdoor primary cells the bug was masked by GetNearbyObjects's
radial sweep. For indoor primary cells (where #98 gates the outdoor
sweep), the door's outdoor cell 0xA9B40029 never reached
portalReachableCells, the door's BSP was never queried, and the player
walked through Holtburg cottage doors unimpeded.
Fix: AddAllOutsideCells treats worldSphereCenter as landblock-local
directly. Matches retail CLandCell::add_all_outside_cells which uses
the per-cell 6-byte landblock-relative position struct.
Existing CellTransitAddAllOutsideCellsTests + CellTransitFindCellSetTests
updated to use landblock-local sphere coords (they were the only callers
using the world-coord convention; production never did).
Apparatus shipped:
- DoorBugTrajectoryReplayTests — live-capture-driven replay harness
that pinpointed the bug per-field at unit-test speed (<500ms iteration)
- AddAllOutsideCells_LandblockLocalSphere_AddsDoorOutdoorCell — direct
unit test that demonstrates the fix
- FindTransitCellsSphere_IndoorExitPortal_AddsOutsideForCapturedSpherePos
— verifies cell-portal traversal at the captured sphere position
- DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection
— dat-direct EnvCell + Environment.Cells + portal-poly inspector
- Fixture: tests/AcDream.Core.Tests/Fixtures/door-bug/live-capture.jsonl
(tick 13558 walkthrough + tick 22760 outdoor block)
Visual verification (user-driven at Holtburg cottage door, ~50cm off-center):
- outside→inside RUN: now BLOCKS (was: walks through)
- outside→inside WALK: presumed blocks (not retested)
- inside→outside RUN: PARTIAL — body intersects door, sphere slides through
- inside→outside WALK: same partial behavior
The remaining inside→outside asymmetry is a SEPARATE bug in BSP
collision response for two-sided polygons. The [bsp-test] probe now
fires 245 times for the door entity from indoor (was 0 pre-fix) —
door IS being queried; the BSP polygon-level collision response is
the new bug. Handoff at
docs/research/2026-05-25-door-bug-partial-fix-shipped.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triage step from the plan at C:\Users\erikn\.claude\plans\
i-did-some-work-sharded-acorn.md. Four sessions on issue #98 left the
worktree dirty with ~1352 LOC of mixed work. This commit splits the
work into "keep" (defensible + diagnostic) and "drop" (failed
experiments), then commits the keep set with the drops removed.
Plan asked for three commits (diag / fix / revert); consolidated to one
because the diagnostic emits in TransitionTypes.cs are tightly
interleaved with the multi-sphere CellTransit calls and the CellId
switch. Hunk-level splitting in those files for marginal bisect
granularity didn't justify the misclick risk.
Reverted entirely (failed experiments per slice 7 handoff):
- src/AcDream.Core/Physics/PhysicsDataCache.cs — neg-poly storage
fields (Stippling, PosSurface, NegSurface, HasNegativeSide,
IsNegativeSide, NegativeSide).
- src/AcDream.Core/Physics/ShadowObjectRegistry.cs — isBuilding flag
propagation through Register / ShadowEntry.
- tests/AcDream.Core.Tests/Physics/BSPQueryTests.cs — 165 lines of
PolygonWithNegativeSide_* tests.
- tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs —
isBuilding propagation tests.
- src/AcDream.Core/World/WorldEntity.cs — IsLandblockBuilding field
(no consumer once ShadowObjectRegistry.isBuilding is gone).
- src/AcDream.Core/World/LandblockLoader.cs — IsLandblockBuilding=true
setter on building entities (kept BuildBuildingTerrainCells).
- src/AcDream.App/Rendering/GameWindow.cs — isBuilding: arg passed to
ShadowObjects.Register.
- src/AcDream.Core/Physics/BSPQuery.cs — TryAdjustWalkableSide /
IsWalkableAt helpers, their callers, the Path 5 / Path 6 neg-poly
branch split, the BldgCheck-tied clearCell conditional, and the
neg-poly ResolveCellPolygons writes.
- src/AcDream.Core/Physics/PhysicsDiagnostics.cs — neg-poly fields
in the poly-dump format.
- src/AcDream.Core/Physics/TransitionTypes.cs — SpherePath.BldgCheck +
SpherePath.HitsInteriorCell fields and every consumer, the
savedBldgCheck try/finally around FindCollisions, and the neg-poly
format additions to the dump-on-error helper.
- src/AcDream.Core/Physics/CellTransit.cs — FindCellSet overloads
with hitsInteriorCell out-param and the BuildCellSetAndPickContaining
out-param threading.
Kept (defensible correctness fixes + diagnostic infrastructure):
- src/AcDream.App/Rendering/GameWindow.cs — render-vs-physics cell
origin split: the 0.02m render lift no longer leaks into physics
BSP caching. lb.BuildingTerrainCells threaded into LandblockMesh.Build.
- src/AcDream.Core/World/LoadedLandblock.cs — BuildingTerrainCells
record field.
- src/AcDream.Core/World/LandblockLoader.cs — BuildBuildingTerrainCells
(cy*8+cx from LandBlockInfo.Buildings).
- src/AcDream.Core/Terrain/LandblockMesh.cs — hiddenTerrainCells
param that collapses owned-cell triangles to a zero-area degenerate.
- src/AcDream.App/Streaming/{GpuWorldState,LandblockStreamer}.cs —
mechanical BuildingTerrainCells threading through LoadedLandblock
reconstructions.
- src/AcDream.Core/Physics/CellTransit.cs — multi-sphere
FindTransitCellsSphere variant + multi-sphere AddAllOutsideCells +
FindCellSet(IReadOnlyList<Sphere>, …) overload + the
BSPQuery.SphereIntersectsCellBsp call for loaded neighbours. Matches
retail CObjCell::find_cell_list / CEnvCell::find_transit_cells.
- src/AcDream.Core/Physics/TransitionTypes.cs — multi-sphere FindCellSet
call site, retail-faithful CellId switch after CheckOtherCells, the
outdoor-landcell terrain-walkable fallback in CheckOtherCells, and
the full diagnostic suite ([step-walk], [walkable-nearest],
[issue98-walkable-detail], [cell-set-summary], LastBspHitPoly
emits).
- src/AcDream.Core/Physics/PhysicsDiagnostics.cs — ProbeStepWalkEnabled
gate (ACDREAM_PROBE_STEP_WALK=1) + LogStepWalk helper + FormatVector
/ FormatPlane utilities. All emit-gated.
- src/AcDream.Core/Physics/BSPQuery.cs — diagnostic emits to
LastBspHitPoly at four sites in SphereIntersectsPolyInternal /
the placement adjustment path.
- Test files for the kept work: CellTransitFindCellSetTests,
CellTransitFindTransitCellsSphereTests, PhysicsDiagnosticsTests,
TransitionCheckOtherCellsTests, LandblockMeshTests,
LandblockLoaderTests.
Verification:
- dotnet build: green, 0 errors, 3 pre-existing warnings.
- dotnet test: 1156 passed + 8 failed (baseline was 1148 + 8 pre-
existing; the +8 passing are the new tests for the kept defensible
work). Same 8 pre-existing failures, no new regressions.
Backup of pre-triage worktree state in stash@{0}.
A6.P3 #98 is still open; this is the apparatus-prep step, not a fix.
Next: cell-dump probe (Step 2 of the plan).
Outdoor→indoor entry path used PointInsideCellBsp (point-only) for the
building-portal containment test. When the player logs in INSIDE a
building and the foot-sphere center is just past the destination cell's
CellBSP boundary, the point-only check failed → CellId stuck as
outdoor → indoor BSP queries never ran → walls passable. User-reported
symptom: "logged in in the inn, at start ran through exterior walls,
ran back in and they block now."
Fix: swap PointInsideCellBsp for SphereIntersectsCellBsp (the radius-
aware port from #90). Promotes CellId to the interior cell the moment
ANY part of the foot-sphere crosses the destination cell boundary —
matches retail's CCellStruct::sphere_intersects_cell timing at
acclient_2013_pseudo_c.txt:317666 exactly.
The sphereRadius parameter was already plumbed through CheckBuildingTransit
per #89's documented "future upgrade" note from 2026-05-19 (which is
exactly today's symptom). Closes#89.
1147 + 8 baseline maintained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactors FindCellList to delegate to a private helper
(BuildCellSetAndPickContaining) that returns BOTH the containing cell
id AND the full candidate HashSet. Public surface gains a new
FindCellSet overload; existing FindCellList behavior is unchanged.
Used by the upcoming Transition.CheckOtherCells (Phase A4) to iterate
every cell the sphere overlaps for per-cell BSP collision. Mirrors
retail's CObjCell::find_cell_list filling both cell_array AND var_4c
at acclient_2013_pseudo_c.txt:272725.
Three new unit tests cover sphere-fully-inside-primary,
sphere-straddling-portal, and outdoor-seed-neighbour-landcells cases.
Spec: docs/superpowers/specs/2026-05-20-phase-a4-multi-cell-bsp-design.md
Plan: docs/superpowers/plans/2026-05-20-phase-a4-multi-cell-bsp.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual test of Phase 2 portal traversal showed walls still didn't
block from inside buildings. Diagnosis: ResolveCellId was being
called with sp.CheckPos (entity reference, at the feet — world
Z=terrain) instead of sp.GlobalSphere[0].Origin (foot sphere center,
~0.5m above terrain). Combined with the +0.02f Z-bump on cached
cell origins (for render z-fight prevention), the test position
landed at cell-local Z=-0.02 — just below the cell floor — and
PointInsideCellBsp correctly reported "outside" for every cell.
CheckBuildingTransit never added candidates; player CellId stayed
outdoor; indoor cell-BSP collision branch never fired; walls didn't
block.
Retail's check_building_transit uses sphere.Center (the sphere CENTER,
not the entity reference) per the pseudocode at
docs/research/acclient_indoor_transitions_pseudocode.md:222-238.
Three call sites updated (PhysicsEngine x2 inside ResolveWithTransition;
TransitionTypes inside Transition.FindEnvCollisions).
Also adds a [check-bldg] diagnostic line to CheckBuildingTransit (gated
on the existing ACDREAM_PROBE_INDOOR_BSP flag) so future verification
captures show per-portal inside/outside results without needing
another diagnostic flag.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five reviewer-flagged items addressed:
- Fix#1: GameWindow building-loop now reuses TerrainSurface.ComputeOutdoorCellId
instead of re-deriving the row-major cell-index formula. DRY win; no risk
of the two formulas drifting.
- Fix#2: BuildingPhysics.ExactMatch decoder now references
DatReaderWriter.Enums.PortalFlags.ExactMatch instead of magic 0x0001.
- Fix#3: ExactMatch XML doc clarified as "reserved per retail's
CBldPortal::exact_match; not currently consumed by CheckBuildingTransit".
- Fix#4: CheckBuildingTransit docstring now explicitly documents the
retail divergence — retail's sphere_intersects_cell (radius-aware) vs.
our PointInsideCellBsp (radius-less). The sphereRadius parameter is
reserved for the future sphere_intersects_cell port. Practical effect
noted: entry fires ~sphereRadius (~0.48m) deeper than retail.
- Fix#5: Test method `SphereInsideBuildingPortalDestination_AddsInteriorCell`
renamed to `BuildingPortalWithUnloadedCellBSP_NoCandidateAdded` — the
test asserts Empty(candidates), not that the cell is added. Comment
updated.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the outdoor→indoor entry path. New BuildingPhysics type holds
the per-SortCell BldPortal list + building world transform; PhysicsDataCache
caches it (CacheBuilding + GetBuilding); CellTransit.CheckBuildingTransit
tests each portal's destination cell via PointInsideCellBsp.
PhysicsEngine.ResolveCellId's outdoor branch now hooks CheckBuildingTransit
after the terrain-grid lookup: if the matched landcell has a cached
building stab, check whether the sphere has crossed into one of its
interior EnvCells before returning.
GameWindow at landblock-load time iterates LandBlockInfo.Buildings and
caches each via PhysicsDataCache.CacheBuilding. The landcell-id derivation
uses retail's row-major cell-index formula (gridX * 8 + gridY + 1).
Polish items from Subagent B/C reviews folded in:
- visited HashSet in FindCellList's BFS (avoids O(N^2) re-enqueue)
- ResolveCellId_NoDataCache_ReturnsFallback test (closes coverage gap)
- DataCache-asymmetry comment in PhysicsEngine.ResolveCellId
- Replaced misleading FindCellList outdoor-branch TODO with explicit
note that ResolveCellId bypasses this branch — wired in ResolveCellId
directly.
- Removed unused 'using DatReaderWriter.Types;' from CellTransit.cs
- 2 new CellTransitFindCellListTests integration tests
- 1 new CellTransitCheckBuildingTransitTests test (null-CellBSP guard
case; happy path deferred to visual verification).
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New CellTransit static class ports retail's portal-graph cell traversal:
- FindTransitCellsSphere — indoor portal-neighbour walk
- AddAllOutsideCells — outdoor 24m grid expansion
- FindCellList — top-level driver (BFS through portals;
PointInsideCellBsp for final containment)
PhysicsEngine.ResolveOutdoorCellId renamed to ResolveCellId. Body
rewritten: indoor seeds delegate to CellTransit.FindCellList (portal-
graph BFS + BSP containment test); outdoor seeds keep the landblock
terrain grid lookup from the original implementation (preserving the
L.2e prefix-preservation fix). Signature extended with sphereRadius
parameter (needed by the sphere-vs-portal-plane test). Three call
sites updated (PhysicsEngine x2, TransitionTypes x1).
BSPQuery.PointInsideCellBsp retyped from PhysicsBSPNode? to CellBSPNode?
— the function operates on the cell-BSP tree (CellPhysics.CellBSP.Root
is a CellBSPNode). The previous PhysicsBSPNode typing was dead code, so
retype is safe.
Deletes the Phase D ResolveOutdoorCellIdTests.cs file. New ResolveCellIdTests
covers the equivalent contracts (fallback zero, outdoor seed with no
landblock).
Outdoor->indoor entry (check_building_transit) is stubbed pending the
BuildingPhysics infrastructure landing in the next commit.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>