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