Commit graph

1214 commits

Author SHA1 Message Date
Erik
a5d2244467 docs(handoff): Phase A8 RR1 shipped — pickup prompt for RR2 spike
Session-end handoff capturing:
- RR0 findings + design + plan + RR1 cleanup all shipped (8 commits)
- Working tree at logical R2-baseline + [vis] probe
- RR2 (BuildingInfo spike) is next; ~30-60 min; human-in-the-loop step
  for live-inspect
- Canonical doc-read order for fresh session
- Pickup prompt with state-both-altitudes header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:46:32 +02:00
Erik
29e306b0f6 docs: Phase A8 — mark prior restructure design+plan as SUPERSEDED
Both documents retained for historical reference. The new full-WB-port
design + plan (2026-05-26-phase-a8-wb-full-port-design.md + plan, ea60d1f +
651e7e2) replace them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:08:48 +02:00
Erik
fd721afdf9 Revert "feat(render): Phase A8 R3 — wire stencil pipeline into render frame (WB order)"
This reverts commit 60f07bc21b.
2026-05-27 10:08:10 +02:00
Erik
b93103885a Revert "fix(render): Phase A8 R3.5 — gate stencil branch on PointInCell containment"
This reverts commit 38d537491f.
2026-05-27 10:07:15 +02:00
Erik
664ca9cb16 Revert "fix(render): Phase A8 R3.5 v2 — gate depth-clear on cameraReallyInside too"
This reverts commit 2bfeafd358.
2026-05-27 10:07:15 +02:00
Erik
84c4a70296 diag(render): Phase A8 [vis] probe — light up dormant ProbeVisibilityEnabled
Wires the dormant RenderingDiagnostics.ProbeVisibilityEnabled flag (added
2026-05-25 by Task 6 of the original A8 plan, no probe code) to per-frame
[vis] log lines around the render-frame branch decision. Captures camera
position, cameraInsideCell (lenient grace-aware), the strict PointInCell
result, the visibility CameraCell id, and VisibleCellIds count/list.

Enable via ACDREAM_PROBE_VIS=1.

Used during A8 RR0 falsification spike (2026-05-26) — see
docs/research/2026-05-26-a8-rr0-falsification-findings.md. Kept as long-
term diagnostic for the upcoming RR8/RR10 visual verification gates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:07:10 +02:00
Erik
651e7e22fb docs(plan): Phase A8 — full WB RenderInsideOut + RenderOutsideIn port plan
12-task implementation plan (RR1-RR12, 94 step checkboxes total):
  RR1  — Cleanup: commit [vis] probe; revert R3+R3.5 v1+v2; supersede old docs
  RR2  — Spike: confirm BuildingInfo shape + WB interior-portal walk algorithm
  RR3  — Implement Building + BuildingRegistry + BuildingLoader (TDD, 10 tests)
  RR4  — Wire registry into landblock load + LoadedCell.BuildingId
  RR5  — WbDrawDispatcher.Draw(cellIds:) overload (TDD)
  RR6  — IndoorCellStencilPipeline 3-bit + occlusion-query helpers
  RR7  — Render frame: WB Steps 1-4 + outdoor branch + stencil-gated sky
  RR8  — Visual verification gate: Steps 1-4 close #78 + Issues A+C
  RR9  — Step 5 (3-stencil-bit cross-building + occlusion queries)
  RR10 — Visual verification gate: Step 5
  RR11 — RenderOutsideIn (cottage interiors through windows from outside)
  RR12 — Final visual matrix + ship docs (close #78, #102; update CLAUDE.md)

Each task: bite-sized 2-5 min steps; exact code snippets; commit per task.
Visual gates at RR8, RR10, RR12 ensure each layer works before adding the
next. Risk register handles RR2 data-shape uncertainty + RR9/RR11 frustum
API adaptation.

Estimated 8-10 sessions (~1.5-2 weeks calendar). Closes M1.5 indoor world
acceptance scope.

Design: docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:57:45 +02:00
Erik
ea60d1fb7d docs(spec): Phase A8 — full WB RenderInsideOut + RenderOutsideIn port (design)
Re-brainstormed after RR0 falsification showed R3+R3.5 introduced
Issues A+C (rendering all 16 BFS-reachable cells at full screen extent
caused co-planar Z-fight + grace-state leak from outside view). The
prior design's "WB-faithful restructure" was insufficient — it kept
the BFS-wide cell rendering. Retail and WB both solve indoor visibility
with per-portal recursive culling.

This design ports WB's full pipeline:
  - RenderInsideOut Steps 1-5 (including 3-stencil-bit cross-building)
  - RenderOutsideIn (cottage interiors visible through windows from outside)
  - Per-building cell association (Building + BuildingRegistry, plus
    LoadedCell.BuildingId for O(1) cell→building lookups)
  - Single strict cameraInsideBuilding gate (no grace for render path)
  - Stencil-gated sky inside indoor branch (acdream enhancement)

12 tasks (RR1-RR12), 8-10 sessions estimated. M1.5 indoor scope ships fully.

Supersedes:
  docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md
  docs/superpowers/plans/2026-05-26-phase-a8-restructure.md
(both will be footer-marked in RR1 cleanup)

Reverts in RR1: R3 (60f07bc), R3.5 v1 (38d5374), R3.5 v2 (2bfeafd).
R1+R2 (data layer + dispatcher partition) stay — orthogonal infrastructure.

RR2 spike resolves the BuildingInfo data shape + interior-portal walk
algorithm against WB PortalRenderManager:518-551 before RR3 implements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:29:14 +02:00
Erik
f9bab501df docs(research): Phase A8 RR0 — Issues A + C caused by R3, NOT pre-existing
Three-branch falsification spike per the design's RR0:

  HEAD (2bfeafd, R3.5 v2):     Issue C YES; Issue A YES (varies by building)
  R3 baseline (60f07bc):        Issue C YES; Issue A YES (same as HEAD)
  main (7034be9, no A8 work):   Issue C NO; Issue A NO flicker; BUT
                                 constant #78 symptom (houses-below-terrain
                                 visible from inside)

Diagnosis: R3 (stencil pipeline wire-in) successfully fixes the original
#78 main symptom but introduces Issues A and C as new transition artifacts.
R3.5 v1+v2 patches didn't help (R3 baseline shows same A+C as HEAD).

Per the design's decision gate (Outcome 2): PAUSE plan; re-brainstorm
via superpowers:brainstorming to address A+C without re-introducing #78
constant leak.

The original restructure design assumed A+C might be pre-existing and
could be filed as separate out-of-A8-scope issues. RR0 invalidates that.
The restructure must address them OR the brainstorm needs a third option
between "stencil-gate everything" (causes A+C) and "no stencil work"
(causes #78).

Open questions for the re-brainstorm captured in the findings doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:33:32 +02:00
Erik
769a003138 docs(plan): Phase A8 — render-frame restructure implementation plan
Six-task plan implementing the approved design:
  RR0 — pre-restructure falsification spike (3-branch repro of A + C)
  RR1 — revert R3.5 v1 + v2 (38d5374 + 2bfeafd)
  RR2 — restructure render frame to WB-faithful order
  RR3 — verify SkyRenderer doesn't toggle stencil state
  RR4 — visual verification matrix (4 buildings + transitions + sky)
  RR5 — ship docs (close #78, file new follow-ups, update CLAUDE.md)

Bite-sized steps (2-5 min each) with exact code snippets, commands,
and expected outcomes. TDD where applicable; GL integration tasks are
visual-verification-only by nature.

Each task has explicit decision gates:
  RR0-S5 — outcome 2 (A8-caused) triggers re-brainstorm
  RR3-S1 — dirty SkyRenderer triggers wrapper variant
  RR4-S9 — building-type failure triggers /investigate

Design: docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:48:46 +02:00
Erik
732f766d1b docs(spec): Phase A8 — render-frame restructure to WB-faithful order (design)
Brainstorm-approved design for the A8 R3.5 → restructure pivot. Replaces
the R3.5 v1+v2 frankenstein (terrain twice + depth-clear workaround) with
WB's RenderInsideOut order verbatim: skip initial sky+terrain when inside,
delete the depth-clear, add a stencil-gated sky step inside the indoor
branch so windows show real sky (closes R4 Issue B).

Unifies the two-flag asymmetry (cameraInsideCell lenient + cameraReallyInside
strict) into a single strict cameraInside flag via PointInCell. Grace
mechanism in CellVisibility stays alive for non-render consumers.

Six tasks ahead, in order:
  RR0 — pre-restructure falsification spike (Issues A + C on main?)
  RR1 — revert R3.5 v1+v2 (38d5374 + 2bfeafd)
  RR2 — restructure render frame to WB-faithful order
  RR3 — verify SkyRenderer doesn't toggle stencil state
  RR4 — visual verification matrix (cottage/cellar/inn/dungeon + transitions)
  RR5 — ship docs (close #78; file new follow-ups if pre-existing on main)

Next: superpowers:writing-plans to produce the per-task plan.

Note: the design references two predecessor docs that are currently
untracked in this worktree (entity-taxonomy + phase-a8-replan). Their
contents are read-stable on disk; committing them is a separate concern
(they belong to the prior session's work). The handoff doc this design
continues from is at f90fa2f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:49:18 +02:00
Erik
f90fa2f863 docs(handoff): Phase A8 R3.5 paused for restructure — fresh-session handoff
R1 + R2 + R3 + R3.5 v1 + R3.5 v2 all shipped this session (ed727042bfeafd). Primary #78 fix works (cottage walls solid from inside). Three
transition / sky issues remain that resist symptom-level patching:

  A — Exit indoor→outdoor: "objects through ground + building parts missing"
  B — Inside through window: "sky doesn't render"
  C — Entry outdoor→indoor: "floor transparent showing cellar + wrong texture"

Root cause: architectural mismatch with WB's RenderInsideOut reference.
We draw initial terrain unconditionally + depth-clear-if-inside as a
workaround; WB skips initial terrain when inside and renders terrain
ONLY at the stencil-gated step. The R3.5 v1+v2 patches were symptom
fixes that kept producing new edge cases — the exact "patching symptoms"
anti-pattern the predecessor revert handoff called out.

Handoff doc captures: what shipped, what works, what doesn't (with
verbatim user reports), the architectural diagnosis (WB vs our pipeline),
the recommended next-session approach (brainstorm → write-plan → execute
with the full superpowers workflow), and a self-contained pickup prompt.

No code changes in this commit — handoff is doc-only. The 5 implementation
commits (ed727042bfeafd) remain at HEAD; next session decides whether
to revert R3.5 v1+v2 for a cleaner diff vs the R3 baseline, or layer
the restructure on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:52:46 +02:00
Erik
2bfeafd358 fix(render): Phase A8 R3.5 v2 — gate depth-clear on cameraReallyInside too
R3.5 v1 only gated the stencil branch on `cameraReallyInside`; the
depth-clear-if-inside at line ~7129 stayed on `cameraInsideCell`. During
grace frames after exit:

  cameraInsideCell   = true  (grace, holds previous cell for 3 frames)
  cameraReallyInside = false (PointInCell on camera pos returns false)

So depth-clear FIRED (writing depth = 1.0 globally) but the OUTDOOR branch
ran (single Draw(All) on every entity). With depth cleared, terrain's
depth = 1.0 — every entity below terrain (cellar geometry, basement
GfxObjs, anything at world Z < terrain Z) won the depth test and rendered
THROUGH the ground. User reported: "stand outside or pass outside → flicker
where objects are visible through ground and walls of other buildings are
missing."

v2 fix: unify depth-related gates on `cameraReallyInside`. During grace
frames depth-clear is now ALSO skipped; terrain depth survives; the
outdoor pass renders normally with proper terrain occlusion. Sky /
lighting / particles continue to use `cameraInsideCell` for smooth
grace-aware transitions.

The two-flag split is now explicit:
  cameraInsideCell    → sky, lighting (smooth, grace-aware)
  cameraReallyInside  → depth-clear, stencil branch (strict, no grace)

Closes the persistent transition flicker observed in R4 visual
verification after v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:40:35 +02:00
Erik
38d537491f fix(render): Phase A8 R3.5 — gate stencil branch on PointInCell containment
Closes the transition-flicker symptom observed during R4 visual verification:
brief 1-3 frames after exiting a building where outdoor scenery rendered
with wrong stencil mask, "walls disappear and buildings show under ground"
shimmer, and sky stayed suppressed.

Root cause: CellVisibility.FindCameraCell holds the previous CameraCell
for ~3 grace frames after the camera physically exits the cell volume
(see _cellSwitchGraceFrames). The grace mechanism prevents flicker at
the doorway threshold for sky/lighting/depth-clear, but the new R3
stencil branch was using `cameraInsideCell` directly — so during grace
frames it ran MarkAndPunch with the previous cell's portals (now behind/
beside the camera) and the IndoorPass + stencil-gated outdoor produced
the garbage frame.

Fix: compute `cameraReallyInside` via the stricter
CellVisibility.PointInCell containment check and use it (instead of
`cameraInsideCell`) as the gate for the stencil branch. Sky, depth-clear,
lighting, and particles continue to use `cameraInsideCell` so their
smooth grace-aware behavior is unchanged.

Handoff item #10 (docs/research/2026-05-26-a8-revert-handoff.md) flagged
this exact concern: "Likely the CellSwitchGraceFrameCount = 3 interacting
with stencil setup timing." Confirmed and closed.

Visual-verification of the fix is part of R4 (re-run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:07:36 +02:00
Erik
60f07bc21b feat(render): Phase A8 R3 — wire stencil pipeline into render frame (WB order)
Replaces the pre-A8 single dispatcher call with the WB RenderInsideOut
order when cameraInsideCell:

  1. Terrain draws normally (color + depth)
  2. depth-clear-if-inside (depth = 1.0 globally)
  3. MarkAndPunch — stencil bit 1 at camera's-own-cell exit portals
  4. IndoorPass — cell mesh + cell statics + building shells, stencil OFF
  5. EnableOutdoorPass + re-draw terrain + OutdoorScenery, stencil-gated
  6. DisableStencil + LiveDynamic, depth-test only

Outdoor (cameraInsideCell == false) path unchanged: single Draw(set: All).

Step 5 (WB's 3-stencil-bit cross-cell-portal pipeline) is DEFERRED — we
mark only the camera's own cell's exit portals via [visibility.CameraCell],
not the BFS-extended VisibleCellIds. Trade-off documented in
docs/research/2026-05-26-a8-entity-taxonomy.md §"open questions".

Adds IndoorCellStencilPipeline field + ctor wiring + Dispose. Field types
the partition consumers from R2; the ParentCellId / IsBuildingShell /
ServerGuid distinctions are now consumed at runtime.

Visual verification at cottage interior / cottage cellar / inn interior /
dungeon is R4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:46:45 +02:00
Erik
55f26f2a9c feat(render): Phase A8 R2 — WbDrawDispatcher.EntitySet taxonomy partition
Reshapes the dormant EntitySet enum from binary IndoorOnly/OutdoorOnly to
a three-way taxonomy-aware partition:

  IndoorPass     — cell mesh + cell statics + building shells
                   (ParentCellId.HasValue OR IsBuildingShell), live-dynamic
                   excluded
  OutdoorScenery — outdoor scenery only (ParentCellId == null AND
                   !IsBuildingShell), live-dynamic excluded
  LiveDynamic    — ServerGuid != 0 (player, NPCs, dropped items)

Centralizes the membership predicate in EntityMatchesSet to keep the three
call sites (two in WalkEntitiesInto, one in WalkEntitiesForTest) DRY.

R1's IsBuildingShell flag is now consumed at render time. Integration into
the render frame ships in R3.

Tests rebuilt from scratch — 7 cases cover the new partition truth table.
Existing dispatcher tests (Tier 1 cache, etc.) continue to pass under the
default EntitySet.All.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:42:09 +02:00
Erik
ed72704f7b feat(world): Phase A8 R1 — tag WorldEntity.IsBuildingShell at LandblockLoader
Adds a bool flag at the WorldEntity data layer set by LandblockLoader from
the source dat array: LandBlockInfo.Buildings → true (cottage walls, inn
walls, smithy walls); LandBlockInfo.Objects → false (trees, lampposts,
rocks, hitching posts).

Retail anchor: CLandBlock::init_buildings reads a separate BuildInfo**
array from objects (acclient.h:31893 num_buildings / buildings field;
acclient_2013_pseudo_c.txt:313854 init_buildings entry). WorldBuilder
preserves the same distinction via SceneryInstance.IsBuilding
(StaticObjectRenderManager.cs:334). Today acdream's loader reads both
arrays into the same WorldEntity pool with no tag, destroying the
distinction (the comment at GameWindow.cs:5175 already acknowledges this
gap for scenery suppression). This commit closes the gap.

Render-time consumption arrives in R2 (EntitySet partition refactor).
Two new LandblockLoader tests lock the tagging behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:31:11 +02:00
Erik
d2db8d5b22 docs: Phase A8 REVERT handoff — full session story + pickup prompt
Documents the 3-round visual verification failure of the original
A8 plan, the architectural taxonomy gap that surfaced (cottage walls
are landblock-baked stabs with ParentCellId == null, not cell mesh,
so the binary IndoorOnly/OutdoorOnly partition mis-classifies them),
and what the re-plan must consider.

Bottom line: the WB stencil approach is correct in principle and the
infrastructure (Tasks 1-6: PortalPolygons field, RenderingDiagnostics
flag, portal_stencil shaders, IndoorCellStencilPipeline,
PortalMeshBuilder, EntitySet enum) is correct and tested. The
integration (Task 7) made a wrong architectural assumption about
entity classification. Reverted by fef6c61, 96f8bd2, c897a17.

Includes detailed pickup prompt for the re-plan session: re-investigate
entity taxonomy (6 distinct classes documented), spike distinguisher
options (AABB-encloses-camera heuristic recommended for first ship),
re-plan Task 7 with MarkAndPunch-first GL order + separate live-entity
pass + 3-building visual verification requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:42:53 +02:00
Erik
fef6c619a9 Revert "feat(render): Phase A8 — wire stencil pipeline into render frame"
This reverts commit 41c2e67cd8.
2026-05-26 09:38:37 +02:00
Erik
96f8bd2bd7 Revert "fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass"
This reverts commit a2ad5c1ac4.
2026-05-26 09:38:37 +02:00
Erik
c897a179fa Revert "fix(render): Phase A8 — mark-and-punch BEFORE indoor draw (correct WB order)"
This reverts commit b76f6d112e.
2026-05-26 09:38:36 +02:00
Erik
b76f6d112e fix(render): Phase A8 — mark-and-punch BEFORE indoor draw (correct WB order)
Second visual verification surfaced three depth-ordering bugs all from
one cause: the IndoorOnly dispatcher Draw ran BEFORE MarkAndPunch, so
the far-depth punch (gl_FragDepth = 1.0 at stencil=1 portal silhouettes)
overwrote any indoor depth that had been written there. Result:

  • Closed doors leaked outside terrain — door mesh wrote depth 0.6 at
    the portal silhouette, then the punch overwrote it to 1.0, then
    terrain at 0.99 won the depth test.
  • Walls between rooms leaked the far-side door/window opening —
    same mechanism: wall depth at the far-portal silhouette destroyed
    by the punch.
  • Animated character body bled to terrain where it overlapped a
    portal silhouette on screen — same mechanism: character depth
    destroyed by the punch.

Re-reading WB's RenderInsideOut (VisibilityManager.cs:73-239) confirms
the correct order is mark-and-punch FIRST, then indoor cells. Indoor
geometry drawn AFTER the punch wins the depth test against 1.0 and
correctly occludes the subsequent stencil-gated outdoor pass.

The swap is a single block move; MarkAndPunch was already correctly
leaving the GL state stencil-disabled for the indoor pass to follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:18:13 +02:00
Erik
a2ad5c1ac4 fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass
Visual verification of A8 (commit 41c2e67) surfaced a showstopper:
player + NPCs disappeared when the camera entered a building. Root
cause: live server-spawned entities (animated player/NPCs/monsters)
have ParentCellId == null. The EntitySet partition classified them
as "outdoor" and stencil-gated them in the OutdoorOnly pass — so
they only rendered where stencil bit 1 was set (portal silhouettes),
producing partial-body and head-backwards artifacts at doorway
transits and full invisibility everywhere else inside.

Fix: animatedEntityIds overrides the ParentCellId-based partition.
Animated entities always belong in the IndoorOnly pass (stencil OFF),
never in OutdoorOnly. Three changes:
- WalkEntitiesInto full-walk path: compute isAnimated up front, use
  it in both partition checks
- WalkEntitiesInto animated-only path: skip the entire path on
  OutdoorOnly (every iterated entity is animated by definition)
- WalkEntitiesForTest: add optional animatedEntityIds parameter,
  mirror the new partition logic

Two new tests cover:
- EntitySet_IndoorOnly_IncludesAnimatedEntitiesEvenWithNullParentCellId
- EntitySet_OutdoorOnly_ExcludesAnimatedEntities

Known remaining limitation: dropped items / static-but-live objects
have ParentCellId == null AND are NOT in animatedEntityIds, so they
still classify as outdoor scenery and stencil-gate. Addressing this
requires a "live entity" flag on WorldEntity — deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:55:45 +02:00
Erik
41c2e67cd8 feat(render): Phase A8 — wire stencil pipeline into render frame
Replaces the pre-A8 "terrain-always + depth-clear-when-inside" pattern
with WB's stencil-aware ordering when cameraInsideCell:

  1. Upload portal triangle mesh from VisibleCellIds → LoadedCell.
  2. Draw indoor entities (EntitySet.IndoorOnly) — stencil OFF.
  3. Mark portal stencil + punch far depth (MarkAndPunch).
  4. Draw terrain — stencil-gated to portal silhouettes.
  5. Draw outdoor entities (EntitySet.OutdoorOnly) — stencil-gated.
  6. DisableStencil before particles/weather/UI.

Outdoor path unchanged (EntitySet.All, no stencil work).

Adds CellVisibility.TryGetCell(uint) for the VisibleCellIds → LoadedCell
materialization. Removes the now-redundant DepthBufferBit Clear that
was the old approximation.

Retail oracle: PView::DrawCells at
acclient_2013_pseudo_c.txt:432709 ("outside_view.view_count > 0" gate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:29:45 +02:00
Erik
dcf69a1feb feat(render): Phase A8 — WbDrawDispatcher.EntitySet partition
Adds EntitySet { All, IndoorOnly, OutdoorOnly } and a Draw parameter to
partition the per-entity walk by ParentCellId presence. EntitySet.All
preserves pre-A8 behavior; IndoorOnly drops null-ParentCellId entities;
OutdoorOnly drops ParentCellId.HasValue entities. The visibleCellIds
filter is still applied on top.

Used by Task 7 to split the render frame's single Draw call into two
(indoor stencil-OFF, outdoor stencil-gated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:20:45 +02:00
Erik
a1c393ee14 hardening(render): Phase A8 — IndoorCellStencilPipeline robustness
Three small improvements from Task 5 code review:
- MarkAndPunch now enables DepthTest explicitly (was relying on
  GameWindow's startup enable; this makes the method self-contained).
- Uniform location fields marked readonly (set once in ctor).
- AllocateVbo gets a comment noting that mid-session reallocation is
  safe because the VAO bakes the VBO association at ConfigureVao time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:16:42 +02:00
Erik
3973596468 feat(render): Phase A8 — IndoorCellStencilPipeline + PortalMeshBuilder
The pipeline class owns the portal_stencil shader + a dynamic VBO/VAO
for per-frame portal triangle uploads. MarkAndPunch runs WB's two-step
stencil setup (mark portals = 1, then write gl_FragDepth=1.0 into
stencil=1 regions). EnableOutdoorPass switches to read-only stencil
for the subsequent terrain + outdoor-entity passes.

PortalMeshBuilder.BuildTriangles is the pure-math triangle-fan
extractor — unit-testable without a GL context. Only exit portals
(OtherCellId == 0xFFFF) are emitted; inner portals are skipped to
prevent outdoor geometry from bleeding into adjacent rooms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:02:14 +02:00
Erik
f3d7b13664 docs(render): Phase A8 — document portal_stencil.vert pos.w omission
WB's PortalStencil.vert has a pos.w clamp for the camera-coplanar-with-
portal degenerate. We exclude it per spec (matches retail intent), but
the file should note the omission so future readers don't wonder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:58:26 +02:00
Erik
344034bcd3 fix(render): Phase A8 — remove over-engineered shader guards (Task 4)
Removes the pos.w clamp in portal_stencil.vert and the FragColor
declaration in portal_stencil.frag added in 2d31d49. Both were
speculative defensive code not in the spec or the WB reference. The
shaders now match the spec verbatim (except the locally-conventional
`core` profile qualifier which is correct).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:55:15 +02:00
Erik
2d31d490d1 feat(render): Phase A8 — portal_stencil vert/frag shaders
Minimal pair for the indoor-cell stencil pipeline (#78). Vert transforms
world-space portal polygon vertices through uViewProjection; includes a
near-zero pos.w guard for coplanar-camera robustness (matches WB pattern).
Frag either passes through gl_FragCoord.z or writes gl_FragDepth=1.0
based on uWriteFarDepth; FragColor declared but suppressed via ColorMask
on the CPU side.

Matches WorldBuilder's PortalStencil.vert/.frag at
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Shaders/.
Uses #version 430 core consistent with acdream's mesh_modern shaders.
Deployed to bin/ via existing Rendering\Shaders\*.* .csproj glob.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:52:55 +02:00
Erik
6577c0a21c feat(render): Phase A8 — RenderingDiagnostics.ProbeVisibilityEnabled
Adds the ACDREAM_PROBE_VIS=1 env-var-toggleable flag for the indoor-cell
visibility culling pipeline (#78). Mirrors the existing ProbeIndoor*
pattern. DebugVM checkbox follows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:47:56 +02:00
Erik
d834188a4e feat(render): Phase A8 — populate LoadedCell.PortalPolygons
BuildLoadedCell now reads the full portal polygon vertices from
cellStruct.Polygons[portal.PolygonId].VertexIds and stores them in
local-space on the LoadedCell. Empty arrays for unresolved polygons.
Same source as the ClipPlane block; no new dat read.

Unit test covers the data-class invariant (parallel indexing) since
the full integration is exercised only at runtime with live dat data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:43:24 +02:00
Erik
fee878f292 feat(render): Phase A8 — LoadedCell.PortalPolygons field
First slice of the indoor-cell visibility culling pipeline (#78). Adds
PortalPolygons: List<Vector3[]> to LoadedCell, parallel-indexed to the
existing Portals + ClipPlanes lists. Empty arrays for portals whose
polygon could not be resolved. Field is populated in Task 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:39:31 +02:00
Erik
4cbfbf98af docs: #100 ship + indoor-cell culling investigation handoff
Session-end documentation for the issue #100 ship and the visibility-
culling investigation handoff for the next session.

Three documents land together:

  - docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md
    (the 3-task plan that drove this session's f48c74a / a64e6f2 /
    84e3b72 — never committed by Tasks 1-2)

  - docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md
    (the predecessor session's smoking-gun research that drove the
    #100 fix — never committed by the prior session)

  - docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md
    (THIS session's handoff: what shipped, what visual-verification
    surfaced, the issue family map for #78 + #95 + the new cellar-
    stairs finding, root-cause hypothesis, retail anchors, WB
    references, do-not-retry list, and pickup prompt for the next
    session's investigation + plan + implementation)

Plus two updates to existing files:

  - CLAUDE.md — adds a ship paragraph for #100 to the M1.5 progress
    block. References the new handoff doc as the next-session pickup
    point.

  - docs/ISSUES.md #78 — broadens scope from "outdoor stabs visible
    through floor" to "outdoor stabs + terrain mesh visible inside
    EnvCells". Adds the 2026-05-25 cellar-stairs evidence (per user
    direction: not filed as new issue; treated as evidence
    reinforcing #78's hypothesis #2). Promotes hypothesis #2 to
    "high confidence as of 2026-05-25" and adds the retail anchor
    (acclient_2013_pseudo_c.txt:311397 CEnvCell::find_visible_child_cell).
    Acceptance criteria broadened to include the cellar-stairs case.

Next session: pickup prompt at the bottom of the new handoff doc
drives a /investigate → writing-plans → subagent-driven-development
pass on indoor-cell visibility culling — the work that closes #78
+ cellar-stairs together, and possibly #95 if the infrastructure
overlaps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:17:51 +02:00
Erik
84e3b72b27 docs: #100 — stabilize Task 2 SHA reference in ISSUES.md
Task 2's commit landed at 64518d59, then an amend (to fix the original
placeholder SHA in the same file) produced the new HEAD a64e6f2 with
identical content. The in-file SHA still pointed at the pre-amend
64518d59 — reachable today only via reflog, unreachable after the next
git gc. Switch to a64e6f2 which is on the branch and survives gc.

This is a follow-up commit (not an amend) so the canonical SHA is
itself stable on the branch from this commit forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:47:37 +02:00
Erik
a64e6f20da refactor: #100 — remove hiddenTerrainCells / BuildingTerrainCells plumbing
Retired in favour of Task 1's retail-faithful terrain shader Z nudge.
Pure removal — ~50 LOC of dead surface area across:

  - src/AcDream.Core/Terrain/LandblockMesh.cs (drop parameter +
    cell-collapse block)
  - src/AcDream.Core/World/LoadedLandblock.cs (drop field)
  - src/AcDream.Core/World/LandblockLoader.cs (drop method + call)
  - src/AcDream.App/Rendering/GameWindow.cs (3 sites)
  - src/AcDream.App/Streaming/GpuWorldState.cs (6 ctor sites)
  - src/AcDream.App/Streaming/LandblockStreamer.cs (1 ctor site)
  - tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs (drop test)
  - tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs (drop test)

No retail anchor — the deleted mechanism never had one; this commit
rolls our code back to the actual retail behaviour established in
the prior commit's shader nudge.

ISSUES.md #100 moved to Recently closed.

Cross-ref:
  docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md
  docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:37:53 +02:00
Erik
f48c74aa8b fix(render): #100 — render terrain 1 cm below physical Z (retail zFightTerrainAdjust)
Subtract 0.01 from every terrain vertex Z in the modern terrain vertex
shader, matching retail's per-draw nudge applied inside
ACRender::landPolysDraw(arg2=2). Coplanar building floors now always win
the depth test against the rendered terrain, so the visual "ground at
the building floor" reads as the building's floor, not as Z-fighting.

Constant 0.01f bit-equals retail's float literal 0.00999999978 when
rounded to single precision.

Render-only — physics reads the un-nudged heightmap via
TerrainSurface.SampleZ / SampleZFromHeightmap. The same render-vs-
physics split is already established for EnvCell render lift
(+0.02m at GameWindow.cs around the cell-mesh draw).

Retail anchors:
  docs/research/named-retail/acclient_2013_pseudo_c.txt:1120769
  docs/research/named-retail/acclient_2013_pseudo_c.txt:702254

Cross-ref:
  docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md
  docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md

Followed by Task 2 (delete the hiddenTerrainCells / BuildingTerrainCells
plumbing). Visible result of this commit alone: building floors stop
Z-fighting, but the 24m x 24m transparent rectangles persist until the
plumbing is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:24:28 +02:00
Erik
2fc312eac3 docs: #101 — fix fabricated content in Recently closed entry
Code-review found four Important findings in the ISSUES.md entry
that landed in 381561f:
- broken relative link to the plan (../superpowers/... vs
  superpowers/...)
- wrong test class name (PhysicsDataCacheTests, the actual
  class is PhysicsDataCachePhantomSourceTests)
- wrong predicate description (referenced PhysicsRadius and
  vAabbR; the predicate only checks the high byte and the
  cached BSP root)
- fabricated method names (GameWindow.RegisterGfxObjShadow and
  ShadowShapeBuilder.FromGfxObj — neither exists)

This commit corrects all four. The verification evidence in
the entry was accurate and is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:43:02 +02:00
Erik
381561f5cf docs: #101 — A6.P8 ship + ISSUES.md update (Outcome A)
Records the A6.P8 mesh-aabb-fallback suppression ship outcome
in CLAUDE.md and moves issue #101 to Recently closed. Visual
verification confirmed end-to-end ramp climb on GfxObj 0x01000C16's
BSP (walkable inclined polygon, Normal.Z=0.717).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:31:30 +02:00
Erik
6ca872feba docs(test): #101 — sync stale GameWindow.cs line ref in test class doc
Task 2's 11-line insertion shifted the synthesis gate from line
6116 to 6127. The implementation XML doc was updated in the same
commit; the parallel test-class-level reference was missed.
Code-review minor finding; one-character fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:56:35 +02:00
Erik
5240d654df fix(physics): #101 — suppress mesh-aabb-fallback for phantom GfxObj stabs
The 10 stair-step cyls (entities 0x40B5008A..0x40B50095 in Holtburg
cells 0xA9B40159/A) are synthesized by the mesh-aabb-fallback path
from the visual mesh AABB of GfxObj 0x0100081A — which has
HasPhysics=False and no PhysicsBSP. Retail's CPartArray::InitParts
emits no collision in this case; acdream now matches that by
consulting PhysicsDataCache.IsPhantomGfxObjSource (added in the
previous commit) and skipping synthesis when the predicate fires.

The actual staircase collision is on entity 0x40B50089 (GfxObj
0x01000C16, hasPhys=True, BSP radius 2.645m) — same staircase BSP
that retail uses. After this fix, only that BSP fires; the
phantoms are gone.

Visual verification pending (next step in plan); the BSP dump
from ACDREAM_DUMP_GFXOBJS=0x01000C16 will confirm whether
0x01000C16 has walkable inclined polys for the climb to actually
land. If not, a follow-up issue is needed; the cyl phantom is
closed either way.

Also updates PhysicsDataCache.cs XML doc line reference from
6116 to 6127 (drifted by the 11-line isPhantomGfxObj block
inserted above the guarded if).

Refs docs/research/2026-05-25-a6-stairs-cyl-retail-investigation.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:51:45 +02:00
Erik
f6305b1e3c feat(physics): #101 — add IsPhantomGfxObjSource predicate
Retail's CPartArray::InitParts emits collision shapes only from
Setup-level CylSpheres/Spheres or per-Part PhysicsBSP — never
from visual mesh AABBs. The predicate captures the retail rule:
a stab whose source is a GfxObj (high byte 0x01) with no cached
GfxObjPhysics is phantom (no collision). Wired into GameWindow's
mesh-aabb-fallback synthesis in the next commit.

Refs docs/research/2026-05-25-a6-stairs-cyl-retail-investigation.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:45:18 +02:00
Erik
8795655250 docs: issue #101 — broken stairs cyl phantom (post-A6.P7 finding)
Visual verification of A6.P7 at Holtburg cottage door passed cleanly
(1187 [cyl-skip-bsp] guard fires, 0 [cyl-test] on doors, 30/30
axis-aligned hits, smooth NE/SE slide along door face). While
exploring post-verification, the user discovered a different
staircase in cells 0xA9B40159 + 0xA9B4015A where the sphere cannot
climb at all.

Captured working baseline (stairs-working.jsonl, cottage cellar
stairs in cells 0xA9B40143/146/147 — clean ↔ Z=90.95-94.00 traversal)
and broken scenario (stairs-broken.jsonl, Z stays at 94.00 the entire
4216-record capture).

Root cause is NOT a regression of A6.P7. It's a different bug shape:
the staircase is built as a multi-part EnvCell entity (entityId
0x0040B500, ~150 parts), with 10 of those parts being 0.80m-radius
cylinders forming the steps. Each cyl carries state=0x00000000 — no
HAS_PHYSICS_BSP_PS — so A6.P7's BspOnlyDispatch guard correctly
doesn't fire. Cyl height 0.80m exceeds A6.P6's step-up budget 0.60m
so grounded step-over fails. Falls through to wall-slide which
produces the same diagonal radial phantom A6.P7 closed for the door.

The [resolve-bldg] lines reveal gfxObj=0x0100081A hasPhys=False
bspR=0.00 vAabbR=0.82 — the underlying GfxObj has NO physics BSP;
we appear to be synthesizing a cyl from the visual AABB radius. That
synthesis path is the suspected misregistration.

Filed as issue #101 with severity HIGH. Investigation handoff written
covering 4 retail-research questions (cdb on retail at this stair
location, Setup trace via entity-source probe, ShadowShapeBuilder
vAabbR fallback audit, cell BSP poly dump), do-not-retry list, and 3
candidate fix shapes (don't synthesize cyl from vAabbR / cell BSP for
stairs / cyl-height-tolerant step-over). The handoff explicitly
defers implementation to a later session pending retail evidence.

Files:
- docs/research/2026-05-25-stairs-cyl-investigation-handoff.md (new)
- docs/ISSUES.md — added #101 entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:03:58 +02:00
Erik
888272aad1 fix(phys): A6.P7 — retail-binary cyl-vs-BSP dispatch (HAS_PHYSICS_BSP_PS gate)
Closes the door-cyl phantom slide where a sphere approaching a closed
cottage door at NE/SE headings could be blocked by the cyl's radial
normal contaminating the slide tangent into the slab face (live
evidence in door-a6p6-v2.utf8.log: 12 resolves with
cn=(0.86,0.51,0) attributed to door entity 0x000F4245).

Retail anchor: CPhysicsObj::FindObjCollisions at
acclient_2013_pseudo_c.txt:276861 dispatches BINARILY between
BSP-only and cyl+sphere based on HAS_PHYSICS_BSP_PS (0x10000 in
acclient.h:2833). For non-PvP, non-missile movers — every M1.5
scope walking-vs-static scenario — an entity with the flag set
tests its BSP exclusively; the foot cyl is never tested. ACE
confirms the truth table at PhysicsObj.cs:412-450 (HasPhysicsBSP,
missileIgnore, exemption).

Our dispatcher iterated every ShadowEntry independently and tested
both the cyl AND the BSP for a closed door. Cyl was registered
first (FromSetup walk order), and its diagonal radial slide normal
"won" attribution at the early-return on first non-OK. Result was
out=in for tangential motion along the door face.

Changes (~15 LOC + 7 unit tests):
- PhysicsStateFlags.HasPhysicsBsp = 0x00010000 (PhysicsBody.cs)
- Transition.BspOnlyDispatch(uint state) static predicate
  (TransitionTypes.cs) — mirrors retail's branch with M1.5 scope
  defaults (ebp_1 and eax_12 treated as false; wire PvP / missile
  refinements when those scopes ship)
- Per-entry guard in FindObjCollisions cyl/sphere branch
  (TransitionTypes.cs:2433) — continue when BspOnlyDispatch fires,
  with [cyl-skip-bsp] diagnostic line gated on ProbeBuildingEnabled
- A6P7DispatchRulesTests (7 tests, all GREEN): flag value + 6
  parameterized predicate cases

Verification: 14-test keep-green list from the 2026-05-25 handoff
passes (5 BSPQueryTests.FindCollisions_Path5_*, 2 CellTransitTests.A6P5_*,
2 DoorCollisionApparatusTests.Apparatus_DeadCenter_*,
5 DoorBugTrajectoryReplayTests, 1
CellarUpTrajectoryReplayTests.LiveCompare_FirstCap_FixClosesCottageFloorCap).
Total: 20/20 pass including the new 7-test predicate suite.

The DocumentsBug test (Apparatus_Grounded_50cmOffCenter) fails
post-fix BUT was already failing pre-fix in the worktree baseline
(verified by stashing the fix and re-running — same failure mode:
sphere blocks at start with floor normal (0,0,1)). Not in the
keep-green list, so this is a known pre-existing condition; the
test's own header comment instructs flipping the assertion when
the fix lands.

Investigation:
docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md

Needs visual verification at Holtburg cottage door (NE/SE approach
should now slide smoothly along the door face — zero [cyl-test]
log lines attributed to door entity, replaced by [cyl-skip-bsp]).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:35:32 +02:00
Erik
b36eff1c10 docs(handoff): A6.P7 door-cyl + slab interaction — retail investigation needed
A6.P5 (cellSet fix, 3b1ae83) + A6.P6 (cyl step-over, 3d4e63f) shipped
and verified. Original phantom radial-push is gone. Residual symptom:
sphere blocked at NE/SE headings approaching closed cottage door
because cyl's radial normal drives slide direction into the slab.

Handoff covers:
  - What landed today (don't redo)
  - Concrete evidence from door-a6p6-v2.utf8.log (12 resolves with
    cn=(0.86,0.51,0) on door entity post-A6.P6)
  - 3 fix options (BSP-first per-entity / per-physobj dispatch port /
    door-cyl-informational)
  - 3 retail investigation questions for next session (state bit
    0x10000 semantics, cdb trace on door cyl in retail, Setup parsing
    comparison)
  - Files to read first + tests to keep green + do-not-retry list
  - Pickup prompt with brainstorming-only discipline

Next session's deliverable: a research report, NOT an implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:54:59 +02:00
Erik
3d4e63f9c8 fix(phys): A6.P6 — cylinder step-over for Contact movers (CCylSphere::step_sphere_up)
Retail's CCylSphere::intersects_sphere at acclient_2013_pseudo_c.txt
:324626-324641 routes the Contact-state branch through step_sphere_up
(line 324516), not slide. The step-up check at line 324519-324524:

  cyl_clearance = sphere.radius + cyl.height - offset.z
  if (step_up_height < cyl_clearance) → slide  (cyl too tall)
  else → DoStepUp, on failure → step_up_slide

For the cottage door's foot cyl (h=0.20m, r=0.10m) at standing height,
cyl_clearance = 0.30m and player step_up_height = 0.60m, so the sphere
steps over the cyl easily — no radial push-out.

Pre-fix bug (live trace door-phantom.utf8.log 2026-05-25 PM):
when the player slid along the closed cottage door's slab face, the
foot cyl fired Slid with radial outward push at the door's middle X
(cn=(0.64,0.77,0) etc.) — a "phantom collision" that broke the slide.
Cause: A6.P5's cellSet expansion made the door reliably visible from
all approach angles, exposing this pre-existing behavior. Pre-A6.P5
the cyl wasn't visible from many approach angles so the phantom rarely
fired; the underlying mismatch with retail was always there.

Fix: in CylinderCollision, when oi.Contact && !sp.StepUp && !sp.StepDown
and engine is non-null, compute cyl_clearance, and if step_up_height
allows it, call DoStepUp with the cyl's radial collision normal. On
success the sphere is repositioned past the cyl (returns OK). On
failure (no walkable surface beyond — e.g., a wall behind the cyl),
fall back to StepUpSlide which uses SlideSphereInternal's crease
projection — smoother tangent slide than the radial push.

Conformance:
  - All A6P5 unit tests + Path 5 tests + Apparatus_50cmOffCenter_* +
    Apparatus_DeadCenter_* + Directional_OutsideIn/InsideOut + issue #98
    LiveCompare_FirstCap_FixClosesCottageFloorCap pass in isolation.
  - Full Core suite failure count unchanged (17 baseline → 17 with-fix);
    diff is documented static-leak flakiness, no real regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:29:54 +02:00
Erik
3b1ae83931 fix(phys): A6.P5 — unconditional outdoor expansion in CellTransit BFS
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>
2026-05-25 12:53:31 +02:00
Erik
2a890e6bde test(phys): A6.P5 RED — BFS from indoor cell doesn't reach door outdoor cell
Adds CellTransitTests with two A6P5_* unit tests:
  A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell  (RED — the bug)
  A6P5_BuildCellSetFromAlcove_AlsoReachesDoorOutdoorCell   (passes today)

The RED test reproduces the over-penetration tick's cellSet build:
sphere at (132.594, 16.350) in cell 0xA9B4013F, BFS portal-walks to
0xA9B40150 (alcove) but does NOT add the door's outdoor cell 0xA9B40029.
Pre-fix cellSet: 0xA9B4013F, 0xA9B40150, 0xA9B4014C — no outdoor cells.
Sphere wasn't straddling 0xA9B40150's exit portal so exitOutside stayed
false.

Also removes the 3 A6P5_* replay tests added to
DoorBugTrajectoryReplayTests in the previous commit (3253d84's
follow-up). Those tests didn't reproduce the bug — the harness's
BuildFaithfulDoorEngine has no cell fixtures, so cellSet returned empty
and GetNearbyObjects treated it as "no filter" → door always visible
→ over-penetration test trivially passed for the wrong reason. The
CellTransitTests version pins the bug at the BFS layer directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:51:33 +02:00
Erik
82781c272b test(phys): A6.P5 fixture — 3 ticks from live door-stuck capture
over-penetration-capture.jsonl is 3 records extracted from
door-stuck-capture.jsonl: the cell-crossing over-penetration tick
(0xA9B4013F -> 0xA9B40150, sphere committed 0.27m INTO slab), a
stuck-position hit=yes tick, and a stuck-position hit=no tick. Drives
the A6.P5 replay tests that prove the cellSet gate removal closes both
the over-penetration and the intermittent-visibility bugs.

extract-records.ps1 is a one-shot extractor; reusable if we capture more.
Source captures (door-stuck-*.jsonl, door-stuck-*.launch.log) gitignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:43:51 +02:00
Erik
7910d51e7a diag(phys): A6.P5 [cellset-build] probe — log BuildCellSetAndPickContaining output
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>
2026-05-25 12:40:58 +02:00