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>
This commit is contained in:
Erik 2026-05-25 22:17:51 +02:00
parent 84e3b72b27
commit 4cbfbf98af
5 changed files with 1431 additions and 11 deletions

View file

@ -972,6 +972,27 @@ ramp; no phantom diagonal slides (`[cyl-test]` count on `obj=0x40B500*`
post-fix = 0 vs 7101 pre-fix). Spec:
[`docs/superpowers/plans/2026-05-25-issue-101-stairs-cyl-phantom.md`](docs/superpowers/plans/2026-05-25-issue-101-stairs-cyl-phantom.md).
**Issue #100 — Transparent ground around buildings — SHIPPED 2026-05-25 (primary acceptance);
visibility-culling follow-up handed off.** Three commits: `f48c74a` (terrain shader Z nudge,
retail `zFightTerrainAdjust = 0.00999999978` applied per-vertex in `terrain_modern.vert`),
`a64e6f2` (removed ~50 LOC of `hiddenTerrainCells` / `BuildingTerrainCells` plumbing across
LandblockMesh / LoadedLandblock / LandblockLoader / GameWindow / GpuWorldState /
LandblockStreamer + 2 dead tests), `84e3b72` (docs SHA stabilization follow-up).
Visual-verified 2026-05-25 PM at Holtburg: 24m × 24m transparent rectangles around
every cottage are GONE; ground reads as continuous cobblestone / grass. Plan:
[`docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md`](docs/superpowers/plans/2026-05-25-issue-100-terrain-cutout.md);
predecessor research [`docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md`](docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md).
**Secondary finding from visual verification:** outdoor terrain mesh visible inside
cottage cellars at certain camera angles (clears when camera moves closer; gameplay
unaffected). High-confidence root cause: **indoor-cell visibility culling not gating
outdoor terrain** — same family as filed issue #78 (outdoor stabs visible through inn
floor) and #95 (dungeon portal-graph blowup). Per user direction, NOT filed as a new
issue; treated as additional evidence for #78. Next session investigates + ports
retail's `CEnvCell::find_visible_child_cell` (decomp anchor
`acclient_2013_pseudo_c.txt:311397`) and/or WB's `RenderInsideOut` stencil pipeline.
Full handoff with pickup prompt:
[`docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md`](docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md).
**Today's pre-M1.5 baseline (2026-05-20).** Five surgical fixes
shipped to close the user-reported "logged in inside the inn, ran
through walls" bug: A4 (multi-cell BSP iteration, `691493e`),

View file

@ -131,11 +131,11 @@ the indoor-lighting plumbing.
---
## #78 — Outdoor stabs/buildings visible through the rendered floor
## #78 — Outdoor geometry (stabs + terrain mesh) visible inside EnvCells
**Status:** OPEN
**Severity:** HIGH (immediate visual jank now that floors render)
**Filed:** 2026-05-19
**Status:** OPEN — **next-session investigation target (2026-05-25)**
**Severity:** HIGH (immediate visual jank; broadened scope per 2026-05-25 PM finding)
**Filed:** 2026-05-19 (broadened 2026-05-25)
**Component:** rendering, visibility
**Description:** Standing inside Holtburg Inn looking at the floor or
@ -144,30 +144,64 @@ world position + scale — but visible THROUGH the floor and walls. As if
the cell mesh is rendered but doesn't occlude or stencil-cull what's
behind it.
**Additional evidence (2026-05-25 PM, post-#100 visual verification):**
After issue #100 shipped (commits `f48c74a`, `a64e6f2`, `84e3b72`) and
removed the `hiddenTerrainCells` cell-collapse mechanism, the OUTDOOR
TERRAIN MESH is now (correctly per retail) rendered everywhere on the
landblock — including in 3D regions occupied by indoor EnvCell volumes.
Visual verification at a Holtburg cottage cellar showed a sharp-edged
rectangular grass patch (outdoor terrain at Z≈93.99) rendering over the
cellar stair geometry at certain camera angles. Clears when camera
moves closer (cottage walls + stair treads geometrically occlude the
terrain from new vantage points). Gameplay unaffected. **This is the
same root cause as the existing #78 hypothesis #2** ("outdoor stabs not
culled when player in EnvCell"), just with outdoor terrain mesh
affected in addition to outdoor stab entities. Per user direction,
NOT filed as a new issue — additional evidence reinforces #78's
hypothesis #2, broadens scope of the fix to include terrain culling.
**Root cause / status:** Two plausible causes:
1. The `+0.02f` Z bump applied to cell origin at `GameWindow.cs:5362`
pushes the floor mesh 2 cm above terrain, so depth test correctly
occludes terrain. But OUTDOOR STABS (landblock-baked building geometry)
at the same X,Y may have Z values comparable to or higher than the
cell-mesh floor, producing z-fighting / see-through.
2. Outdoor stabs aren't being culled when the player is inside an
2. **(High confidence as of 2026-05-25)** Outdoor geometry (stabs AND
terrain mesh) isn't being culled when the player is inside an
EnvCell — this is the Phase 1 Task 3 deferred work
("Cull outdoor stabs when indoors via VisibleCellIds"). WB has a
`RenderInsideOut` stencil pipeline (`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`)
that acdream never invokes.
that acdream never invokes. Retail anchor:
`docs/research/named-retail/acclient_2013_pseudo_c.txt:311397`
(`CEnvCell::find_visible_child_cell` at address `0x0052dc50`,
called from `acclient_2013_pseudo_c.txt:280028`).
**Files:**
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (per-entity walk —
consider gating outdoor stab entities on visible-cell membership).
the dispatcher already filters by `entity.ParentCellId ∈
visibleCellIds` but outdoor stabs have `ParentCellId == null` so they
always pass; needs an explicit indoor-camera gate).
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (currently
renders all loaded landblock terrain unconditionally; needs
visibility gating when camera resolves to an indoor cell).
- `src/AcDream.App/Rendering/CellVisibility.cs:222+` (`ComputeVisibility`
returns `VisibleCellIds`; the dispatcher already filters by
`entity.ParentCellId ∈ visibleCellIds` but outdoor stabs have
`ParentCellId == null` so they always pass).
returns `VisibleCellIds`; existing portal-LOS infrastructure to build on).
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`
(`RenderInsideOut` pipeline — reference implementation, never invoked).
**Acceptance:** Standing inside a sealed-interior cell, no outdoor
geometry is visible through floor/walls. Standing where a cell has a
real outdoor portal (door open, window) outdoor geometry is correctly
visible through the portal.
visible through the portal. Cellar-stairs case (2026-05-25 finding):
standing in a Holtburg cottage cellar at any camera angle, no outdoor
terrain mesh visible over the stair geometry.
**Research:**
[`docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md`](research/2026-05-25-issue-100-shipped-and-culling-handoff.md)
— full session handoff with cellar-stairs evidence, family map (#78 +
#95 + cellar-stairs), root-cause hypothesis, retail anchors, WB
references, do-not-retry list, and pickup prompt for the
investigation session.
---

View file

@ -0,0 +1,313 @@
# Issue #100 shipped + indoor-cell culling investigation handoff
**Date:** 2026-05-25 PM
**Status:** Issue #100 SHIPPED (visually verified for primary acceptance). Visual verification surfaced a NEW finding in the same family as issue #78 — outdoor terrain mesh visible inside cottage cellars at certain camera angles. Next session: deep investigation + plan + port retail's indoor-cell visibility culling to close the family.
**Branch:** `claude/strange-albattani-3fc83c` (worktree)
**Predecessor handoff:** [docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md](2026-05-25-issue-100-terrain-cutout-handoff.md) (the prior session's smoking-gun research that drove the #100 fix).
---
## TL;DR
**Shipped this session (3 commits):**
- `f48c74a` — Task 1: terrain shader Z nudge (retail `zFightTerrainAdjust = 0.00999999978`)
- `a64e6f2` — Task 2: removed ~50 LOC of `hiddenTerrainCells` / `BuildingTerrainCells` plumbing across 7 source files + 2 test files, closed #100 in ISSUES.md
- `84e3b72` — docs: stabilized Task 2's SHA reference in ISSUES.md (follow-up commit, not amend)
**Visual verification result (Holtburg, live ACE):**
- ✅ **Primary acceptance:** transparent rectangles around houses are GONE. Ground reads as continuous cobblestone / grass around every cottage observed. Issue #100's user-visible symptom is closed.
- ❌ **New finding:** standing inside a cottage cellar with the camera positioned such that the cottage walls don't fully occlude the view, the outdoor terrain mesh renders as a sharp-edged grass rectangle over the cellar stairs and floor. **Clears when the camera moves closer** (camera position changes such that cottage geometry properly occludes). **Gameplay unaffected** — player can walk down/up the stairs normally.
**Root cause hypothesis for the new finding (HIGH CONFIDENCE):** indoor-cell visibility culling is not gating outdoor terrain rendering. The outdoor terrain mesh is now (correctly per retail) rendered everywhere on the 192 m landblock — including in 3D regions occupied by indoor `CEnvCell` volumes. When the camera is in an indoor cell, the outdoor terrain mesh should be EXCLUDED from the draw set unless an outdoor cell is reachable via portal LOS from the camera's cell. acdream does not currently perform this culling.
**This is the same root cause as filed issue #78** ("Outdoor stabs/buildings visible through the rendered floor" at the inn), just with outdoor *terrain* affected instead of outdoor *stabs*. #78 was filed 2026-05-19 with the hypothesis "Outdoor stabs aren't being culled when the player is inside an EnvCell — this is the Phase 1 Task 3 deferred work ('Cull outdoor stabs when indoors via VisibleCellIds')." We never returned to it.
---
## The visibility-culling issue family
Three filed/observed issues likely share infrastructure:
| ID | Symptom | Domain |
|---|---|---|
| **#78** (OPEN) | Inside Holtburg Inn, outdoor stabs/buildings visible THROUGH the floor and walls | Outdoor stabs not culled when camera in indoor cell |
| **Cellar-stairs** (NEW, observed 2026-05-25 PM) | Inside cottage cellar, outdoor terrain mesh visible covering stair geometry at certain camera angles | Outdoor terrain not culled when camera in indoor cell |
| **#95** (OPEN) | Entering dungeon via portal, `visibleCells` per cell jumps from ~4-7 to **135-145**, including cells from other landblocks; see-through walls, other-dungeon geometry visible | Indoor→indoor portal-graph traversal blowup (over-inclusion) |
#78 and the cellar-stairs finding are the **same bug** (outdoor geometry not culled when camera is in an indoor cell) with different geometry classes affected. **They should close together.**
#95 is a sibling — same visibility-culling SUBSYSTEM but different specific failure (indoor→indoor over-inclusion via unrooted portal recursion). It might or might not close as a side effect of the #78/cellar-stairs fix; the next session should determine if the infrastructure overlaps enough to fix both, or whether #95 needs its own work.
Additional adjacent issues (probably NOT same root cause but worth noting):
- **#79, #80, #81, #93, #94** — indoor lighting bugs. Filed under A7 (M1.5 lighting fidelity). Some may share visibility plumbing (e.g., if lights from outdoor entities leak into indoor cells, that's a visibility issue).
---
## Why I'm confident this is culling, not Z-fighting
Three signals, ordered by weight:
1. **Patch geometry is too large.** A Z-precision Z-fight at coplanar 1 cm separation would manifest as a thin ~0.3 m strip on the topmost stair tread (Z=94). The observed patch is sharp-edged rectangular geometry the size of a terrain cell footprint (likely 24 m × 24 m in landblock-local space), covering multiple stair steps and floor area. That's a polygon, not a precision artifact.
2. **"Clears when closer" matches geometric occlusion, not depth precision.** If 1 cm depth-buffer precision were failing, closer camera distance would PASS more cleanly (precision tightens). The user reports the patch clears as they approach the stairs — consistent with cottage walls + stair treads now occluding the terrain in screen space. At 2-5 m camera distance and 24-bit depth buffer, the 1 cm nudge has sub-millimeter resolving power; precision is not the bottleneck.
3. **Exact match for #78's hypothesis #2 mechanism.** #78 ("outdoor stabs visible through cell walls") was filed 2026-05-19 with hypothesis: outdoor stabs aren't culled when player is in an EnvCell; WB has a `RenderInsideOut` stencil pipeline that acdream never invokes. The cellar-stairs case is the same mechanism applied to outdoor terrain mesh.
**One test that could falsify culling-as-cause:** stand at the spot showing the artifact, look at the grass patch, rotate the camera slowly without moving the character. If the patch FLICKERS / shimmers as you turn, that's Z-fight (depth precision unstable across angles). If the patch stays geometrically stable (its polygon edges move predictably with the camera, but it doesn't flicker), that's culling. The screenshot suggested polygon-stable edges — consistent with culling — but rotating the camera is the definitive test, and the next session should do this in the first 60 seconds of visual checking before planning the fix.
---
## Existing apparatus the next session can use
### acdream's current visibility code
**[`src/AcDream.App/Rendering/CellVisibility.cs`](../../src/AcDream.App/Rendering/CellVisibility.cs)** — portal-based interior cell visibility system ported from ACME's `EnvCellManager.cs`. Exposes:
- `FindCameraCell(...)` — resolves which EnvCell the camera is in.
- `PointInCell(...)` — point-in-cell test with `PointInCellEpsilon = 0.01f`.
- `GetVisibleCells(...)` — returns `VisibleCellIds` set for the camera's current cell, via portal-chain traversal.
- `CellSwitchGraceFrameCount = 3` — anti-flicker grace period for cell transitions.
**[`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`](../../src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs)** — per-entity draw filter. Per ISSUES.md #78 line 165, "the dispatcher already filters by `entity.ParentCellId ∈ visibleCellIds` but outdoor stabs have `ParentCellId == null` so they always pass." This is the gate we need to extend.
**[`src/AcDream.App/Rendering/TerrainModernRenderer.cs`](../../src/AcDream.App/Rendering/TerrainModernRenderer.cs)** — terrain dispatcher. Currently renders ALL loaded landblocks unconditionally. Needs to learn about indoor-camera-state to optionally skip outdoor-cell terrain cells.
### Probes available
From CLAUDE.md "Diagnostic env vars":
- `ACDREAM_PROBE_CELL=1` — one `[cell-transit]` line per `PlayerMovementController.CellId` change. Useful for verifying when the camera is in an indoor vs outdoor cell.
- `ACDREAM_PROBE_RESOLVE=1` — full physics resolver trace.
- Runtime-toggleable via the DebugPanel "Diagnostics" section.
No existing probe instruments the rendering visibility decision — the next session might add one (`ACDREAM_PROBE_VIS=1` that logs the camera's resolved cell + `VisibleCellIds` set per N frames).
### Retail oracle anchors
```
docs/research/named-retail/acclient_2013_pseudo_c.txt:311397
CEnvCell::find_visible_child_cell (address 0x0052dc50)
docs/research/named-retail/acclient_2013_pseudo_c.txt:280028
call site: eax_6 = CEnvCell::find_visible_child_cell(eax_5, &__return, arg5);
```
Grep further for `find_visible`, `visibility`, `cull`, `RenderDeviceD3D::DrawBlock`, `ACRender::draw`, etc. The retail render loop's visibility chain — pre-frame walk-down from the camera's cell through portal-visible neighbours — is the target to port.
### WorldBuilder reference
```
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs
references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cs
```
WB has a `RenderInsideOut` mechanism in these files. Per #78's hypothesis, "acdream never invokes" this pipeline. The next session should determine whether to (a) invoke WB's existing code from our render path, (b) port the algorithm to acdream's namespaces, or (c) write a retail-faithful port from the named-retail decomp directly. CLAUDE.md's WB inventory policy applies — read [docs/architecture/worldbuilder-inventory.md](../architecture/worldbuilder-inventory.md) before deciding.
---
## Do-not-retry list for the next session
1. **Don't try to roll back the #100 fix.** The transparent-rectangle bug was a universal symptom on every Holtburg house. The cellar-stairs artifact is conditional and camera-angle-dependent. Reverting #100 trades a worse bug for a less-bad one.
2. **Don't try to solve the cellar-stairs case by lowering the terrain Z further** (e.g., bumping the shader nudge from 0.01 to 0.1 or 1.0). The visible terrain is rendered at its correct Z; the issue is that it's visible AT ALL inside the indoor cell. Bigger nudge doesn't help and would break coplanar-floor disambiguation elsewhere.
3. **Don't try to solve it by hiding terrain cells based on the building footprint again.** That was issue #100's bug — cell-level hiding is too coarse (cottage ~12 m × 12 m in a 24 m × 24 m cell). The right granularity is per-camera-state visibility, not per-cell mesh modification.
4. **Don't try to fix this with depth tricks** (disable depth-write for terrain, etc.) — those break elsewhere and aren't retail-faithful.
5. **Don't conflate this with #82** (some slope terrain lit incorrectly). #82 is about per-vertex normal calculation; the cellar-stairs artifact is about which polygons render at all, not how they're shaded.
6. **Don't try to land a 1-line fix for this.** Indoor-cell visibility culling is a real system to port. Single-line patches at the symptom site (e.g., "if camera in cellar, skip terrain") would close cellar-stairs but not #78 — and would be the kind of workaround CLAUDE.md prohibits. Per the project rule, fix the root cause: port the visibility computation properly.
7. **Don't trust the WB `RenderInsideOut` code blindly.** WB's editor view has known visibility quirks (per the predecessor handoff: "WB has a known Z-fighting issue in the editor view that nobody noticed because it's editor-only"). Cross-reference WB against retail before adopting.
---
## Open questions for the next session to answer
1. **Is the cellar-stairs artifact 100% culling, or partly Z-precision?** The first verification step is the camera-rotation test described above (rotate without moving — flicker = Z-fight, stable = culling). Until this is confirmed, the diagnosis remains "high confidence" but not certain.
2. **Does the #78 + cellar-stairs fix also close #95?** The two are in the same family but #95's specific failure (over-inclusion of indoor cells via portal recursion) might need a separate cap-traversal-depth fix. The next session should map the shared infrastructure before committing to a combined-or-split plan.
3. **What's the right Phase identifier?** M1.5 doesn't have a "visibility" sub-phase yet. A6 is physics; A7 is lighting. Visibility might warrant its own A-letter (A8?) or be slotted under whichever existing structure makes sense. Discuss with user at the start of the next session before naming the work.
4. **Should the cellar-stairs case be documented in #78** as additional evidence, or filed as a separate issue tied to #78? Per user direction (2026-05-25 PM session-end): don't file a new issue; treat as evidence for #78. The next session's investigation should formalize this — possibly by editing #78 to broaden its description to "outdoor geometry (stabs + terrain) visible inside EnvCells."
---
## Pickup prompt for the next session
```
Indoor-cell visibility culling — port retail's mechanism to close
issue #78 (outdoor stabs visible through inn floor) and the new
cellar-stairs visual artifact discovered while visual-verifying
the #100 fix on 2026-05-25.
Read first (in this order):
1. docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md
(this doc — full session handoff with the family map, root-cause
hypothesis, retail anchors, WB references, do-not-retry list)
2. docs/ISSUES.md #78 (the filed issue; same root cause as the
cellar-stairs finding)
3. docs/ISSUES.md #95 (sibling visibility issue; verify whether
it closes as a side effect)
4. CLAUDE.md — search "currently working toward" to refresh state
State both altitudes:
Currently working toward: M1.5 — Indoor world feels right
Current phase: TBD (visibility culling; new sub-phase to name
with the user at session start — possibly A8 if A6=physics,
A7=lighting follow this naming, OR fits under an existing
A6 sub-phase)
## Session flow (three phases, in order)
### Phase 1 — Investigate (use the /investigate skill)
Independently verify the hypothesis and locate the retail mechanism.
Specifically:
a. Run the camera-rotation falsification test on the cellar-stairs
artifact. Stand in a Holtburg cottage cellar at a position where
the grass overlay is visible, rotate the camera slowly without
moving. If the patch stays geometrically stable (polygon edges
move predictably), confirms culling. If it flickers / shimmers,
pivot the diagnosis to Z-precision.
b. Grep named-retail for the visibility chain. Anchors to start
from:
acclient_2013_pseudo_c.txt:311397 — CEnvCell::find_visible_child_cell
acclient_2013_pseudo_c.txt:280028 — call site
Find: RenderDeviceD3D::DrawBlock (around line 430027 per the
#100 predecessor handoff), the visibility computation that
precedes it, and how it gates outdoor-cell rendering when the
camera is in an indoor cell.
c. Read WorldBuilder's visibility implementation:
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs
references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cs
Specifically the RenderInsideOut stencil pipeline that #78
flags as "acdream never invokes." Decide whether to adopt
wholesale, port to our namespaces, or write fresh from
retail.
d. Read acdream's existing visibility code:
src/AcDream.App/Rendering/CellVisibility.cs
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
src/AcDream.App/Rendering/TerrainModernRenderer.cs
Understand the current per-entity gate (filters by
entity.ParentCellId ∈ visibleCellIds, but outdoor stabs
have null ParentCellId so they always pass — that's the bug).
e. Determine whether #95's symptom (visibleCells exploding to
135-145 at network hubs) closes as a side effect or needs
its own work. Read scen5 acdream.log if it's still in the
research tree.
Output of Phase 1: a short report — either "culling is confirmed
and here's the retail anchor / WB code / acdream extension point"
or "diagnosis pivot needed, here's the new shape." Plus a
fix-shape sketch. Get user approval before Phase 2.
### Phase 2 — Plan (use the superpowers:writing-plans skill)
Draft the implementation plan. The shape depends on Phase 1
findings, but likely 4-6 tasks:
- Task 1: Build the diagnostic probe (ACDREAM_PROBE_VIS=1 logging
camera cell + VisibleCellIds + which entities/cells get
rendered) — apparatus first, per CLAUDE.md's "apparatus for
physics bugs" memory note generalized to rendering.
- Task 2: Extend the WbDrawDispatcher per-entity gate to skip
outdoor entities (ParentCellId == null) when the camera's
current cell is indoor AND no outdoor cell is in VisibleCellIds.
- Task 3: Extend the TerrainModernRenderer to skip outdoor
landblocks under the same condition (or to skip individual
cells if the granularity matters — let the retail decomp
decide).
- Task 4: (Possibly) Port the portal-LOS chain that decides
which outdoor cells ARE visible from inside an indoor cell
via doors/windows — so transitions through doorways don't
abruptly cull and re-add geometry. Read retail's clip-plane
portal test for this.
- Task 5: (Possibly) Address #95's traversal-depth cap if
Phase 1 confirms it's not closed by the #78 fix.
- Task 6: Visual verification — at Holtburg cottages (cellar
stairs no longer show terrain), Holtburg Inn (outdoor stabs
no longer visible through walls), and a portal-entry dungeon
(visibleCells stays in a sane range if #95 is in scope).
### Phase 3 — Implement (use superpowers:subagent-driven-development)
Same pattern as the #100 session: fresh subagent per task,
two-stage review per task (spec + code quality), final review
across all commits, visual verification by user as the
acceptance test.
## Constraints
Per CLAUDE.md "no workarounds" rule — fix the root cause, do not
patch symptom sites. Visibility culling is a real system, not a
one-line gate.
Read the do-not-retry list in this handoff doc (7 items) before
starting Phase 2.
Visual verification is the acceptance test. The fix must close
the cellar-stairs artifact AND #78's "outdoor stabs through floor"
AND not regress #100's transparent-rectangle resolution. Be
honest about partial results.
## Reference repo hierarchy reminder
Per CLAUDE.md "Reference repos: cross-check the relevant ones" —
for visibility/culling work, the relevant references are:
- Retail decomp (docs/research/named-retail/) — primary oracle
- WorldBuilder VisibilityManager + GameScene — implementation reference
- ACE has minimal coverage here (it's server-side; client visibility
is not its concern)
- holtburger is TUI, no rendering visibility
- AC2D has fixed-function rendering — limited modern relevance
Cross-reference retail + WB. If they diverge, retail wins.
## What success looks like
After this work lands:
- Standing in a Holtburg cottage cellar at the exact spot of the
2026-05-25 screenshot artifact, no grass overlay on stairs from
ANY camera angle.
- Standing inside Holtburg Inn, no outdoor stabs visible through
floor or walls.
- Entering a dungeon via the Town Network portal, visibleCells
per cell stays in the ~4-15 range (if #95 in scope).
- No regression on issue #100 (no transparent rectangles around
houses).
- dotnet build green; dotnet test failures within the documented
14-23 flaky window.
```
---
## CLAUDE.md update (post-handoff)
Pending. The CLAUDE.md ship paragraph for #100 was deferred to "after visual verification confirms" — visual verification PARTIALLY confirmed (primary acceptance met, secondary artifact in same family as existing #78). The next session can either:
- Add a brief CLAUDE.md ship entry now mentioning #100 closed + cellar-stairs finding linked to #78
- Skip until #78 / cellar-stairs lands, then add a combined paragraph
Recommendation: add it now (issue #100 is genuinely closed by its own criteria). The cellar-stairs work is a NEW investigation, not a continuation of #100.
---
## Files state at session end
```
Branch: claude/strange-albattani-3fc83c
HEAD: 84e3b72 docs: #100 — stabilize Task 2 SHA reference in ISSUES.md
Parent: a64e6f2 refactor: #100 — remove hiddenTerrainCells / BuildingTerrainCells plumbing
Grandparent: f48c74a fix(render): #100 — render terrain 1 cm below physical Z (retail zFightTerrainAdjust)
Before #100: 2fc312e docs: #101 — fix fabricated content in Recently closed entry
Working tree: clean
Untracked: pre-flight-test-baseline.log, issue100-verify-launch.log (logs, can be deleted/gitignored)
```
Both log files are session-scoped; the next session can either delete them or ignore them. They aren't committed.

View file

@ -0,0 +1,406 @@
# Issue #100 — Transparent ground around buildings — investigation handoff
**Date:** 2026-05-25 PM (end of A6.P8 session)
**Status:** Initial research done; **next session is fix-design + implement**. The smoking gun is retail's per-draw `zFightTerrainAdjust = 0.01`. The current acdream code uses a wrong mechanism (cell-level terrain collapse) that creates the transparent rectangles around every Holtburg house.
**Predecessor issue entry:** [`docs/ISSUES.md` #100](../ISSUES.md) (filed 2026-05-24).
---
## TL;DR
The transparent rectangles around every Holtburg house are caused by acdream's
`hiddenTerrainCells` mechanism — a misfire on the Z-fighting problem. The
mechanism collapses entire 24m × 24m outdoor terrain cells to a zero-area
degenerate when any building's `Frame.Origin` lies in them, but cottages are
only ~12m × 12m, so ~75% of each "hidden" cell is bare framebuffer-clear
showing through.
**Retail's mechanism is different and almost trivially small:** retail
**always renders the full terrain mesh, then nudges every terrain vertex Z
down by `0.00999999978 m` (= ~0.01 m) at draw time.** That makes terrain
always lose the depth test against a coplanar building floor — Z-fight
solved, no cells hidden, no cutout polygon needed. Verbatim from the
2013 EoR retail decomp:
| Source | What |
|---|---|
| `docs/research/named-retail/acclient_2013_pseudo_c.txt:1120769` | `float zFightTerrainAdjust = 0.00999999978;` |
| `docs/research/named-retail/acclient_2013_pseudo_c.txt:430113` | `DrawLandCell(esi_3)` — per-cell terrain draw |
| `docs/research/named-retail/acclient_2013_pseudo_c.txt:430124` | `DrawSortCell(esi_3)` — per-cell building draw, **same iteration** |
| `docs/research/named-retail/acclient_2013_pseudo_c.txt:427867` | `ACRender::landPolysDraw(arg2->polygons, 2)` — the `arg2=2` path |
| `docs/research/named-retail/acclient_2013_pseudo_c.txt:006b6402` | `edi_4[1] = (float)((long double)esi_1[2] - (long double)zFightTerrainAdjust);` — the terrain-Z nudge |
**WorldBuilder also renders full terrain** — it does **not** hide cells.
WB has a known Z-fighting issue in the editor view that nobody noticed
because it's editor-only.
[`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TerrainGeometryGenerator.cs:123-141`](../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TerrainGeometryGenerator.cs) iterates all 64 cells unconditionally.
**The fix is path 2 from the issue #100 entry**, refined: drop
`hiddenTerrainCells` entirely + apply `gl_Position.z -= 0.01` (or
equivalent world-Z nudge) in `src/AcDream.App/Rendering/Shaders/terrain_modern.vert`
at line 139. Estimated change: ~15 LOC across 1-2 commits, including
removal of the dead `BuildingTerrainCells` / `hiddenTerrainCells`
plumbing.
---
## Symptom (concrete evidence)
User screenshot 2026-05-25: standing next to a Holtburg cottage. The ground
in a rectangular footprint around the building appears as a flat dark
pink/light patch (the framebuffer clear color) instead of cobblestone /
grass terrain. Visible as a sharp-edged rectangle the size of the
**outdoor terrain cell** (24 × 24 m), not the size of the **cottage's
building footprint** (~12 × 12 m). Same shape on every house observed.
User wording from 2026-05-24 report: "around every house now I missing
the ground texture, it is transparent. I can see through the ground."
---
## Root cause (now confirmed via decomp cross-reference)
### The acdream code that produces the bug
Commit `35b37df` (2026-05-23, A6.P3 #98 triage) kept the
`hiddenTerrainCells` mechanism. The path:
1. **`LandblockLoader.BuildBuildingTerrainCells(LandBlockInfo info)`**
([`src/AcDream.Core/World/LandblockLoader.cs:39-50`](../../src/AcDream.Core/World/LandblockLoader.cs:39))
reads `info.Buildings`, computes
`int cx = clamp(building.Frame.Origin.X / 24f, 0, 7)`,
`int cy = clamp(building.Frame.Origin.Y / 24f, 0, 7)`, and emits
`cy * 8 + cx` per building. Granularity: **one 24m cell per building**.
2. **`LandblockMesh.Build`**
([`src/AcDream.Core/Terrain/LandblockMesh.cs:175-185`](../../src/AcDream.Core/Terrain/LandblockMesh.cs:175))
replaces every index in those cells with the cell's first-vertex index,
producing degenerate (zero-area) triangles that the GPU rasterizer skips.
3. Result: a **24m × 24m hole** in the terrain mesh per building, regardless
of the building's actual size.
A cottage at, say, world `(110, 26)` has `Frame.Origin` at landblock-local
`(110, 26)``cx = 4`, `cy = 1` → outdoor cell index `12`. The hidden
area is `(cx*24, cy*24)` to `((cx+1)*24, (cy+1)*24)` = `(96, 24)` to
`(120, 48)` — a 24×24m square. The cottage footprint is closer to
~12×12m centred near `(110, 26)`. ~75% of the hidden area has no
building geometry to cover it → framebuffer-clear visible.
### What the existing comments said the intent was
[`src/AcDream.Core/Terrain/LandblockMesh.cs:171-174`](../../src/AcDream.Core/Terrain/LandblockMesh.cs:171):
> Indices are trivial 0..383 since we don't deduplicate verts. When a
> building owns an outdoor terrain cell, **keep the fixed 384-index
> contract but collapse its two triangles so the building/stair mesh can
> visually own the hole.**
[`src/AcDream.Core/World/LandblockLoader.cs:33-37`](../../src/AcDream.Core/World/LandblockLoader.cs:33):
> Map LandBlockInfo.Buildings to 8x8 terrain mesh cells (cy * 8 + cx).
> **Retail attaches each CBuildingObj to its outside landcell during
> CLandBlock::init_buildings;** keep this signal separate from stabs so
> ordinary static props do not punch holes in terrain.
The first comment shows the intent: avoid Z-fighting between the building
floor and the terrain below. The second is correct but irrelevant — retail
attaches buildings to a cell for render-order (the `DrawSortCell` step),
NOT to hide that cell's terrain. Our author misread the retail intent.
---
## Retail mechanism (verbatim)
Per the research-agent dispatch this session, the full retail render
sequence is at `RenderDeviceD3D::DrawBlock`
([`acclient_2013_pseudo_c.txt:430027`](../research/named-retail/acclient_2013_pseudo_c.txt)
onwards):
```
for each CLandCell in draw_array (all 64 cells): // line 430113
DrawLandCell(esi_3) // → ACRender::landPolysDraw(polygons, 2)
DrawSortCell(esi_3) // → DrawBuilding(...) for any CBuildingObj attached
// to this cell + the cell's object list
```
`landPolysDraw(polygons, 2)` selects the path that subtracts
`zFightTerrainAdjust` from every terrain vertex Z at upload time. The
constant:
```c
float zFightTerrainAdjust = 0.00999999978; // acclient_2013_pseudo_c.txt:1120769
```
And the application
([`acclient_2013_pseudo_c.txt:006b6402`](../research/named-retail/acclient_2013_pseudo_c.txt)):
```c
edi_4[1] = ((float)(((long double)esi_1[2]) - ((long double)zFightTerrainAdjust)));
```
Where `edi_4[1]` is the output vertex Z and `esi_1[2]` is the source
vertex Z. So every terrain vertex's `Z` becomes `Z - 0.01` at draw time.
**Result:** terrain is uniformly 1 cm lower than its physical height (the
physics path uses the un-nudged Z; only the render path nudges). Building
floors at the physically-correct height always win the depth test
because they're 1 cm higher than the rendered terrain. No cells are
hidden. No cutout is computed. The world reads as one continuous surface.
### Retail's `CLandBlock::init_buildings`
[`acclient_2013_pseudo_c.txt:313854`](../research/named-retail/acclient_2013_pseudo_c.txt)
iterates `lbi->buildings`, calls
`CBuildingObj::makeBuilding(building_id, ...)`, then
`CBuildingObj::add_to_cell(eax_4, landcell)` — attaches the building to
whichever `CLandCell` it physically belongs to. **This is for render
ordering (sort) and physics scoping, not for terrain cutout.** No terrain
modification happens here.
### `BuildInfo` data fields (acclient.h:32035)
```c
struct __cppobj BuildInfo {
IDClass<_tagDataID,32,0> building_id; // Setup DID (0x02xxxxxx)
Frame building_frame; // position + rotation
unsigned int num_leaves; // portal leaf count
unsigned int num_portals;
CBldPortal **portals;
};
```
**There is no explicit footprint polygon, AABB, or terrain-cell list.**
The only geometric anchor is `building_frame.Origin`. Building footprint
must be derived from the Setup's `parts[0]` GfxObj geometry if you needed
it — retail never does, because the depth-nudge mechanism makes it
unnecessary.
---
## Recommended fix shape
### Path 2 (refined) — retail-faithful terrain Z-nudge
**Site:** [`src/AcDream.App/Rendering/Shaders/terrain_modern.vert`](../../src/AcDream.App/Rendering/Shaders/terrain_modern.vert) line 139.
**Change:** replace
```glsl
gl_Position = uProjection * uView * vec4(aPos, 1.0);
```
with
```glsl
// Retail zFightTerrainAdjust (acclient_2013_pseudo_c.txt:1120769, value
// 0.00999999978). Lower terrain by 1 cm so coplanar building floors
// (at the un-nudged physically-correct Z) always win the depth test.
// Cross-ref: docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md.
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
gl_Position = uProjection * uView * vec4(terrainPos, 1.0);
```
**Cleanup (same commit or follow-up):**
1. Delete `hiddenTerrainCells` parameter and the collapse block at
`LandblockMesh.cs:175-185`.
2. Delete `LoadedLandblock.BuildingTerrainCells` field at
`src/AcDream.Core/World/LoadedLandblock.cs`.
3. Delete `BuildBuildingTerrainCells` at
`LandblockLoader.cs:33-50`.
4. Delete the threading through `GameWindow.cs:1808, 5366, 8761` and
`src/AcDream.App/Streaming/{GpuWorldState,LandblockStreamer}.cs`.
5. Delete `tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs`'s
hiddenTerrainCells test cases. Delete or rewrite
`tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs`'s
`BuildBuildingTerrainCells_*` cases.
**Test plan:**
- Add a tiny shader-vertex unit test if there's a precedent (look in
`tests/AcDream.App.Tests/Rendering/` for any shader-correctness tests).
- Visual verification at Holtburg: terrain renders continuously under
cottages, no transparent rectangles. Z-fighting between building floor
and terrain not visible.
- Run the full focused test suite (now 23 tests, will likely shrink by 2-4
when the dead `BuildBuildingTerrainCells` / `LandblockMesh.hiddenTerrainCells`
tests are removed) and confirm green.
**Why this is right:**
- Matches retail mechanism verbatim (1 cm Z nudge on terrain at draw time).
- Removes ~50 LOC of dead plumbing (`BuildingTerrainCells` threading
through 5 files).
- Avoids the per-building-footprint computation that the current code
cannot do correctly without loading the Setup mesh.
### Why NOT path 1 (polygon-level cutout)
- Retail doesn't do this — there is no precedent in the named decomp.
- Building footprint isn't in `BuildInfo` — would require loading the
Setup AND computing a 2D XY footprint polygon from `parts[0]`'s
geometry. Engineering-heavy.
- Even if computed, mesh modifications break the fixed 384-index contract
in `LandblockMesh.Build`.
### Why NOT path 3 (building yard mesh)
- Retail doesn't have this. `BuildInfo` carries no yard polygon.
- Cottage Setups don't appear to include a yard mesh in their geometry
(would need confirmation by dumping a cottage Setup, but the retail
mechanism makes this question moot).
---
## Do-not-retry list
1. **Don't try to compute the building's tight footprint** from
`LandBlockInfo.Buildings`. The struct doesn't carry one. Retail doesn't
either. Any computation would require loading the Setup mesh and
building an XY hull from `parts[0]` — pure engineering with no retail
anchor.
2. **Don't shift the 0.02 m EnvCell render lift** at
`GameWindow.cs:5400` (or equivalent). That lift is for indoor-cell
floor rendering and is correct as-is. The terrain Z nudge is the
reverse direction (lower terrain) and is independent.
3. **Don't disable depth testing** on terrain or building draws. Retail
uses standard depth test (`GL_LESS` equivalent); the Z nudge alone is
the disambiguator.
4. **Don't apply `glPolygonOffset`** to terrain. Retail uses a vertex Z
nudge, not GPU-side polygon offset. Polygon offset has hardware-specific
slope-dependent behavior; the constant 1 cm world-Z is uniform and
well-defined.
5. **Don't keep `hiddenTerrainCells` and add the Z nudge as a "belt and
suspenders"** safety. The hidden-cells path is wrong and should be
deleted in the same commit. Two mechanisms for the same problem is
future technical debt.
6. **Don't touch the physics path.** The Z nudge is render-only. Physics
already uses the un-nudged terrain Z. This is the same render-vs-physics
split that `35b37df` correctly introduced for the `0.02m` EnvCell render
lift (kept item in that commit's "Kept" list).
---
## Files involved (for the next session)
| File | What's there | Action |
|---|---|---|
| `src/AcDream.Core/Terrain/LandblockMesh.cs:175-185` | `hiddenTerrainCells` collapse block | Delete |
| `src/AcDream.Core/Terrain/LandblockMesh.cs:Build` signature | `IReadOnlySet<int>? hiddenTerrainCells` param | Delete param |
| `src/AcDream.Core/World/LoadedLandblock.cs` | `BuildingTerrainCells` field | Delete |
| `src/AcDream.Core/World/LandblockLoader.cs:33-50` | `BuildBuildingTerrainCells` method | Delete |
| `src/AcDream.Core/World/LandblockLoader.cs:Load` | `buildingTerrainCells` local + threading into `LoadedLandblock` ctor | Delete locals + simplify ctor call |
| `src/AcDream.App/Rendering/GameWindow.cs` ~lines 1808, 5366, 8761 | `LandblockMesh.Build(..., lb.BuildingTerrainCells)` call sites | Drop the `hiddenTerrainCells` argument |
| `src/AcDream.App/Streaming/GpuWorldState.cs` | `BuildingTerrainCells` threading | Drop |
| `src/AcDream.App/Streaming/LandblockStreamer.cs` | `BuildingTerrainCells` threading | Drop |
| `src/AcDream.App/Rendering/Shaders/terrain_modern.vert:139` | `gl_Position = ...` | Insert `aPos.z - 0.01` nudge above |
| `tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs` | `hiddenTerrainCells` test cases | Delete |
| `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs` | `BuildBuildingTerrainCells_*` cases | Delete |
---
## Open questions
1. **Old terrain shader removed?** There's a `terrain_modern.vert` and the
build-output mirrors. Confirm there's no older `terrain.vert` that
also needs the nudge applied (the comment at line 4-5 says "Math
identical to terrain.vert"; check whether the legacy shader is still
compiled into the binary or has been fully retired post-N.5b).
2. **Sky / water shaders** — confirm the Z-nudge doesn't accidentally
affect anything else. Should be limited to the terrain shader only.
3. **Building floor render order** — retail also relies on the
`DrawSortCell` per-cell building draw happening after `DrawLandCell`.
Does acdream's current draw order put buildings after terrain? If yes,
nothing else needed. If the order is reversed, the depth-nudge still
works because depth-test is positional, not order-dependent. Just
verify for completeness.
4. **Does WB have a different shader Z nudge we should crib?** The
research agent says no — WB renders full terrain without nudge and
has Z-fighting in the editor view. So we should NOT crib from WB
here; this is one of the cases where WB and retail diverge and
retail wins.
---
## Pickup prompt for next session
```
Issue #100 — Transparent ground around buildings.
Initial research is done by the prior session (the smoking gun is
retail's zFightTerrainAdjust = 0.01). This session: VALIDATE the
research first, then plan, then implement.
Read first (in this order):
1. docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md
(the handoff doc — symptom, retail mechanism, proposed fix
shape, do-not-retry list, files involved)
2. docs/ISSUES.md #100
3. CLAUDE.md — search "currently working toward" to refresh state
State both altitudes:
Currently working toward: M1.5 — Indoor world feels right
Current phase: A6 follow-up — fix issue #100 visual regression
## Session flow (three phases, in order)
### Phase 1 — Investigate (use the /investigate skill)
Independently verify the handoff's claims before committing to the
fix shape. Specifically:
a. Confirm zFightTerrainAdjust = 0.00999999978 at
docs/research/named-retail/acclient_2013_pseudo_c.txt:1120769
and the nudge-application at line 006b6402. The handoff cites
these — read them yourself and cross-check the surrounding
context.
b. Confirm WorldBuilder renders all 64 cells unconditionally at
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/
TerrainGeometryGenerator.cs (handoff says lines 123-141).
c. Read src/AcDream.App/Rendering/Shaders/terrain_modern.vert in
full and confirm line 139 is the right injection point. Check
for any older terrain shader still compiled into the binary
(the handoff flags this as an open question).
d. Check that physics uses the un-nudged Z. Render-vs-physics
split must hold; we cannot let the Z nudge leak into collision.
e. Confirm there's no precedent for glPolygonOffset on terrain
in our codebase (handoff says no, but verify).
Output of this phase: a short report in chat — either "research
confirmed, fix shape stands" or "found X divergence, here's the
revised fix shape." If the research holds, proceed to Phase 2.
### Phase 2 — Plan (use the superpowers:writing-plans skill)
Draft the implementation plan. Expect 3-4 tasks:
Task 1: terrain_modern.vert Z nudge (the one substantive change).
Task 2: delete hiddenTerrainCells / BuildingTerrainCells plumbing
(LandblockMesh.cs, LoadedLandblock.cs, LandblockLoader.cs,
GameWindow.cs call sites, GpuWorldState.cs,
LandblockStreamer.cs). Pure removal — no behavioral
change beyond what Task 1 introduces.
Task 3: delete corresponding tests in LandblockMeshTests +
LandblockLoaderTests that exercise the dead plumbing.
Task 4: visual verification — terrain renders continuously at
Holtburg cottages, no transparent rectangles, no obvious
Z-fighting at building floors.
The handoff doc has a file-by-file action table to seed the plan.
### Phase 3 — Implement (use superpowers:subagent-driven-development)
Execute the plan with fresh subagents per task, two-stage review
between (spec + code quality), final review across all commits.
Pre-flight verification: full focused test suite green. Build clean.
## Constraints
Do-not-retry list in the handoff doc (6 items). Read it before
starting Phase 2.
Visual verification is the acceptance test — the M1.5 milestone is
at stake and any new visual regression in this area would be
obvious. Be honest about what visual verification shows; don't
declare success on partial regressions.
```

View file

@ -0,0 +1,646 @@
# Issue #100 — Transparent Ground Around Buildings — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace acdream's cell-level `hiddenTerrainCells` mechanism (which produces 24m × 24m transparent rectangles around every Holtburg house) with retail's per-vertex Z nudge (`zFightTerrainAdjust = 0.00999999978`). Render terrain everywhere and let coplanar building floors win the depth test by being 1 cm higher than the rendered terrain.
**Architecture:** One-line change to [src/AcDream.App/Rendering/Shaders/terrain_modern.vert:139](src/AcDream.App/Rendering/Shaders/terrain_modern.vert:139) — pre-subtract 0.01 from `aPos.z` before the projection multiply, so every terrain vertex renders 1 cm below its physical Z. Physics path untouched (reads the un-nudged heightmap via [TerrainSurface](src/AcDream.Core/Physics/TerrainSurface.cs)). Then delete the `hiddenTerrainCells` / `BuildingTerrainCells` plumbing that's been threading through `LandblockMesh.Build`, `LoadedLandblock`, `LandblockLoader`, `GameWindow`, `GpuWorldState`, and `LandblockStreamer` — ~50 LOC of dead surface area once the nudge replaces it.
**Tech Stack:** GLSL 460 (terrain shader), C# .NET 10 (Core + App layers), xUnit tests, dotnet CLI.
**Retail oracle:**
- `docs/research/named-retail/acclient_2013_pseudo_c.txt:1120769``float zFightTerrainAdjust = 0.00999999978`
- `docs/research/named-retail/acclient_2013_pseudo_c.txt:702254` (address `006b6402`) — `edi_4[1] = ((float)(((long double)esi_1[2]) - ((long double)zFightTerrainAdjust)));` inside `ACRender::landPolysDraw(arg2=2)`
**Predecessor research:** [docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md](docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md). Phase 1 verification: see chat transcript (research confirmed end-to-end).
---
## Constraints
1. **Render-only.** The Z nudge MUST land in the shader, NOT in `LandblockMesh.Build` vertex output. Physics reads terrain Z from the same source — if we modify the mesh data, physics breaks too.
2. **Constant value `0.01f`.** Match retail's literal `0.00999999978` to single-precision: `0.01f` is bit-identical when round-tripped. Don't use `glPolygonOffset` (slope-dependent, hardware-variable); use a constant world-Z subtract in the vertex shader.
3. **No belt-and-suspenders.** Per [handoff §do-not-retry #5](docs/research/2026-05-25-issue-100-terrain-cutout-handoff.md), don't keep the `hiddenTerrainCells` mechanism alongside the nudge — delete it.
4. **One commit per logical change.** Don't bundle the shader nudge with the plumbing removal — keep the bisect window honest if a regression appears.
5. **Test-suite baseline.** Pre-flight `dotnet test` produces a baseline number for the focused suites (some pre-existing flakiness exists per the A6.P3 evening v2 follow-on note in CLAUDE.md). Each task must not increase the failure count.
---
## File Structure
**Files modified:**
| File | Role | Change |
|---|---|---|
| `src/AcDream.App/Rendering/Shaders/terrain_modern.vert` | Terrain vertex shader | Add Z nudge before projection multiply |
| `src/AcDream.Core/Terrain/LandblockMesh.cs` | Terrain mesh builder | Drop `hiddenTerrainCells` parameter + collapse block |
| `src/AcDream.Core/World/LoadedLandblock.cs` | Loaded-landblock DTO | Drop `BuildingTerrainCells` field |
| `src/AcDream.Core/World/LandblockLoader.cs` | Dat → LoadedLandblock | Drop `BuildBuildingTerrainCells` method + its call |
| `src/AcDream.App/Rendering/GameWindow.cs` | Runtime wiring (3 sites) | Drop the field reference at each Build / ctor site |
| `src/AcDream.App/Streaming/GpuWorldState.cs` | World-state owner (6 ctor sites) | Drop the 4th arg at each ctor site |
| `src/AcDream.App/Streaming/LandblockStreamer.cs` | Worker-side hydration | Drop the 4th arg at the one ctor site |
| `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs` | Loader unit tests | Delete `BuildBuildingTerrainCells_*` test |
| `docs/ISSUES.md` | Issue tracker | Close #100 with the commit SHA |
**Files NOT modified (verified):**
- `src/AcDream.Core/Physics/TerrainSurface.cs` — physics reads un-nudged Z; unaffected.
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs` — consumes `LandblockMeshData` (vertices unchanged).
- `tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs` — no `hiddenTerrainCells` test exists here (handoff was wrong about this surface; the `LandblockMesh.Build` test surface is for triangle/index correctness, not the cell-hide feature).
- All `references/WorldBuilder/**` — read-reference; unchanged.
---
## Pre-flight
- [ ] **Step 0.1: Confirm working tree is on the expected branch**
```powershell
git rev-parse --abbrev-ref HEAD
```
Expected: `claude/strange-albattani-3fc83c` (or whatever branch the user is operating on; not `main`).
- [ ] **Step 0.2: Establish the pre-fix test baseline**
```powershell
dotnet build
```
Expected: `Build succeeded.` (warnings OK, no errors).
```powershell
dotnet test --nologo --no-build --verbosity quiet
```
Capture the total / passed / failed numbers. **Record the baseline failure count** — that's the regression sentinel for Tasks 1 and 2. Per CLAUDE.md, some pre-existing static-state-leak flakiness (819 failures across runs) is known; it's independent of issue #100. The plan's regression check is "failures didn't grow."
---
## Task 1: Terrain shader Z nudge
**Files:**
- Modify: [src/AcDream.App/Rendering/Shaders/terrain_modern.vert:139](src/AcDream.App/Rendering/Shaders/terrain_modern.vert:139)
The single substantive change. After this commit the rectangles around buildings are still there (the `hiddenTerrainCells` plumbing still collapses the terrain inside each building's outdoor cell). Task 2 removes that.
- [ ] **Step 1.1: Edit the terrain vertex shader**
In `src/AcDream.App/Rendering/Shaders/terrain_modern.vert`, replace the final two lines of `void main()` (currently lines 138139, the blank line before `gl_Position` and the `gl_Position` write itself):
```glsl
gl_Position = uProjection * uView * vec4(aPos, 1.0);
```
with:
```glsl
// Retail zFightTerrainAdjust (acclient_2013_pseudo_c.txt:1120769 = 0.00999999978,
// applied per terrain vertex inside ACRender::landPolysDraw at line 702254,
// address 006b6402). Render terrain 1 cm below its physical Z so coplanar
// building floors win the depth test. Physics path is unaffected — it reads
// the un-nudged heightmap via TerrainSurface.SampleZ.
// Closes issue #100; supersedes the hiddenTerrainCells cell-collapse hack.
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
gl_Position = uProjection * uView * vec4(terrainPos, 1.0);
```
- [ ] **Step 1.2: Build to verify the shader file compiles into the binary**
The shader is copied to `bin/Debug/net10.0/Rendering/Shaders/terrain_modern.vert` at build time (it's a `CopyToOutputDirectory` content item). The C# build doesn't statically validate GLSL, but it does confirm the file is in the right place.
```powershell
dotnet build
```
Expected: `Build succeeded.` (warnings OK, 0 errors).
- [ ] **Step 1.3: Run the focused test suites to make sure we haven't broken anything**
```powershell
dotnet test --nologo --no-build --verbosity quiet
```
Expected: total failure count ≤ the baseline from Step 0.2. The shader change cannot affect any non-rendering test, so this is a sanity check that nothing else regressed during the build.
- [ ] **Step 1.4: Commit**
```powershell
git add src/AcDream.App/Rendering/Shaders/terrain_modern.vert
git commit -m @'
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 × 24m transparent rectangles persist until the
plumbing is removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```
---
## Task 2: Remove `hiddenTerrainCells` / `BuildingTerrainCells` plumbing
**Files:**
- Modify: `src/AcDream.Core/Terrain/LandblockMesh.cs` (drop parameter + collapse block)
- Modify: `src/AcDream.Core/World/LoadedLandblock.cs` (drop field)
- Modify: `src/AcDream.Core/World/LandblockLoader.cs` (drop method + call)
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (lines 1809, 5149, 8806)
- Modify: `src/AcDream.App/Streaming/GpuWorldState.cs` (6 ctor sites)
- Modify: `src/AcDream.App/Streaming/LandblockStreamer.cs` (line 231235)
- Modify: `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs` (delete `BuildBuildingTerrainCells_*` test)
Pure removal. The cell-collapse mechanism is the cause of the 24m transparent rectangles around buildings; Task 1's shader nudge replaces it. After this commit the rectangles are gone and terrain renders continuously under every building.
**Order matters within this task:** start at the deepest leaf (`LandblockMesh.Build`'s parameter and collapse block), then work outward through the data type (`LoadedLandblock` record), the loader, and finally the callers. Build at each step to catch shifted-line surprises. The commit at the end captures the whole removal.
- [ ] **Step 2.1: Remove the `hiddenTerrainCells` parameter from `LandblockMesh.Build`**
In `src/AcDream.Core/Terrain/LandblockMesh.cs`:
Delete the docs line for the parameter (currently line 43):
```csharp
/// <param name="hiddenTerrainCells">Optional cell indices (cy * 8 + cx) to draw as zero-area triangles.</param>
```
Change the `Build` signature (currently lines 4451) from:
```csharp
public static LandblockMeshData Build(
LandBlock block,
uint landblockX,
uint landblockY,
float[] heightTable,
TerrainBlendingContext ctx,
System.Collections.Generic.IDictionary<uint, SurfaceInfo> surfaceCache,
System.Collections.Generic.IReadOnlySet<int>? hiddenTerrainCells = null)
```
to:
```csharp
public static LandblockMeshData Build(
LandBlock block,
uint landblockX,
uint landblockY,
float[] heightTable,
TerrainBlendingContext ctx,
System.Collections.Generic.IDictionary<uint, SurfaceInfo> surfaceCache)
```
Replace the index-build loop (currently lines 171185, between the closing `}` of the cell loop and the `return new LandblockMeshData(...)`):
```csharp
// Indices are trivial 0..383 since we don't deduplicate verts. When
// a building owns an outdoor terrain cell, keep the fixed 384-index
// contract but collapse its two triangles so the building/stair mesh
// can visually own the hole.
for (uint i = 0; i < VerticesPerLandblock; i++)
{
int cellIdx = (int)i / VerticesPerCell;
if (hiddenTerrainCells is not null && hiddenTerrainCells.Contains(cellIdx))
{
indices[i] = (uint)(cellIdx * VerticesPerCell);
continue;
}
indices[i] = i;
}
```
with:
```csharp
// Indices are trivial 0..383 since we don't deduplicate verts.
for (uint i = 0; i < VerticesPerLandblock; i++)
indices[i] = i;
```
**Don't build yet** — `LoadedLandblock.BuildingTerrainCells` references will still be present in other files; we'll align them in the next steps before the first build.
- [ ] **Step 2.2: Remove the `BuildingTerrainCells` field from `LoadedLandblock`**
In `src/AcDream.Core/World/LoadedLandblock.cs`, replace the entire file:
```csharp
using DatReaderWriter.DBObjs;
namespace AcDream.Core.World;
public sealed record LoadedLandblock(
uint LandblockId,
LandBlock Heightmap,
IReadOnlyList<WorldEntity> Entities);
```
- [ ] **Step 2.3: Remove `BuildBuildingTerrainCells` from `LandblockLoader` and update the `Load` caller**
In `src/AcDream.Core/World/LandblockLoader.cs`, replace the entire body of the `Load` method (currently lines 1631) with:
```csharp
public static LoadedLandblock? Load(DatCollection dats, uint landblockId)
{
var block = dats.Get<LandBlock>(landblockId);
if (block is null)
return null;
var info = dats.Get<LandBlockInfo>((landblockId & 0xFFFF0000u) | 0xFFFEu);
var entities = info is null
? Array.Empty<WorldEntity>()
: BuildEntitiesFromInfo(info, landblockId);
return new LoadedLandblock(landblockId, block, entities);
}
```
Then delete the `BuildBuildingTerrainCells` method entirely (currently lines 3350, the `/// <summary>` block and the method body). The deleted lines look like:
```csharp
/// <summary>
/// Map LandBlockInfo.Buildings to 8x8 terrain mesh cells (cy * 8 + cx).
/// Retail attaches each CBuildingObj to its outside landcell during
/// CLandBlock::init_buildings; keep this signal separate from stabs so
/// ordinary static props do not punch holes in terrain.
/// </summary>
public static IReadOnlySet<int> BuildBuildingTerrainCells(LandBlockInfo info)
{
var result = new HashSet<int>();
foreach (var building in info.Buildings)
{
int cx = Math.Clamp((int)(building.Frame.Origin.X / 24f), 0, 7);
int cy = Math.Clamp((int)(building.Frame.Origin.Y / 24f), 0, 7);
result.Add(cy * 8 + cx);
}
return result;
}
```
- [ ] **Step 2.4: Update the two `LandblockMesh.Build` call sites in `GameWindow.cs`**
In `src/AcDream.App/Rendering/GameWindow.cs`:
**Site 1 (around line 18081809):** replace
```csharp
return AcDream.Core.Terrain.LandblockMesh.Build(
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache, lb.BuildingTerrainCells);
```
with
```csharp
return AcDream.Core.Terrain.LandblockMesh.Build(
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache);
```
**Site 2 (around line 88058806):** replace
```csharp
return AcDream.Core.Terrain.LandblockMesh.Build(
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache, lb.BuildingTerrainCells);
```
with
```csharp
return AcDream.Core.Terrain.LandblockMesh.Build(
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache);
```
**Site 3 (around line 51455149):** the `new LoadedLandblock(...)` ctor that passes `baseLoaded.BuildingTerrainCells`. Replace
```csharp
return new AcDream.Core.World.LoadedLandblock(
baseLoaded.LandblockId,
baseLoaded.Heightmap,
merged,
baseLoaded.BuildingTerrainCells);
```
with
```csharp
return new AcDream.Core.World.LoadedLandblock(
baseLoaded.LandblockId,
baseLoaded.Heightmap,
merged);
```
- [ ] **Step 2.5: Update the six `LoadedLandblock` ctor sites in `GpuWorldState.cs`**
In `src/AcDream.App/Streaming/GpuWorldState.cs`, each of the six ctor sites currently passes a 4th argument referencing `BuildingTerrainCells`. Drop that argument at each site. The line numbers may shift as edits land — use Grep with pattern `BuildingTerrainCells` to find each site, and edit each one to drop its 4th argument and the trailing comma on the previous line.
For example, the site around line 176180 changes from:
```csharp
landblock = new LoadedLandblock(
landblock.LandblockId,
landblock.Heightmap,
merged,
landblock.BuildingTerrainCells);
```
to:
```csharp
landblock = new LoadedLandblock(
landblock.LandblockId,
landblock.Heightmap,
merged);
```
The site around line 344 is a one-line form:
```csharp
_loaded[kvp.Key] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, newList, lb.BuildingTerrainCells);
```
becomes:
```csharp
_loaded[kvp.Key] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, newList);
```
Apply the same transform to all six sites. After this step there must be **zero** matches for `BuildingTerrainCells` in `src/AcDream.App/Streaming/GpuWorldState.cs`.
- [ ] **Step 2.6: Update the one `LoadedLandblock` ctor site in `LandblockStreamer.cs`**
In `src/AcDream.App/Streaming/LandblockStreamer.cs`, around line 231235, replace
```csharp
lb = new LoadedLandblock(
lb.LandblockId,
lb.Heightmap,
System.Array.Empty<AcDream.Core.World.WorldEntity>(),
lb.BuildingTerrainCells);
```
with
```csharp
lb = new LoadedLandblock(
lb.LandblockId,
lb.Heightmap,
System.Array.Empty<AcDream.Core.World.WorldEntity>());
```
- [ ] **Step 2.7: Delete the `BuildBuildingTerrainCells_*` test**
In `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs`, delete the test method `BuildBuildingTerrainCells_UsesBuildingsOnlyAndMapsToMeshCellIndex` (currently lines 120147 — the `[Fact]` attribute, the method declaration, the body, and the trailing blank line). The deleted block looks like:
```csharp
[Fact]
public void BuildBuildingTerrainCells_UsesBuildingsOnlyAndMapsToMeshCellIndex()
{
var info = new LandBlockInfo
{
Objects =
{
new Stab
{
Id = 0x02000001u,
Frame = new Frame { Origin = new Vector3(120, 72, 0) },
},
},
Buildings =
{
new BuildingInfo
{
ModelId = 0x020000AAu,
Frame = new Frame { Origin = new Vector3(141.5f, 7.2f, 94f) },
},
},
};
var cells = LandblockLoader.BuildBuildingTerrainCells(info);
Assert.Single(cells);
Assert.Contains(5, cells); // cy=0, cx=5 => mesh index cy * 8 + cx.
}
```
Leave the rest of the test class untouched.
- [ ] **Step 2.8: Sweep for any remaining `BuildingTerrainCells` / `hiddenTerrainCells` / `BuildBuildingTerrainCells` references**
```powershell
# Should return ONLY hits in docs/ (handoff doc, ISSUES.md historical entries, archived plans).
# Zero hits in src/ and tests/.
```
Use Grep with pattern `BuildingTerrainCells|hiddenTerrainCells|BuildBuildingTerrainCells` over the repo. Confirm `src/` and `tests/` are clean; `docs/` may still reference these names historically and that's fine.
- [ ] **Step 2.9: Build + test**
```powershell
dotnet build
```
Expected: `Build succeeded.` 0 errors. Warnings should be similar in count to the pre-flight baseline (no new warnings introduced — if a warning appears, the removal missed a site).
```powershell
dotnet test --nologo --no-build --verbosity quiet
```
Expected: failure count ≤ baseline minus 1 (the deleted `BuildBuildingTerrainCells_*` test was passing, so failure count stays at the baseline; total count drops by 1).
- [ ] **Step 2.10: Close issue #100 in `docs/ISSUES.md`**
In `docs/ISSUES.md`, locate the `#100 — Transparent rectangular patches around every house (terrain rendering)` block (around line 764) and update its **Status:** line from:
```markdown
**Status:** OPEN
```
to:
```markdown
**Status:** DONE
**Closed:** 2026-05-25
**Commits:** `<TASK_1_COMMIT_SHA>`, `<TASK_2_COMMIT_SHA>`
```
Replace `<TASK_1_COMMIT_SHA>` with the first 7 chars of Task 1's commit, and `<TASK_2_COMMIT_SHA>` with the first 7 chars of this task's commit (you'll know it after Step 2.11 — re-edit ISSUES.md if needed, OR get the SHAs via `git log --oneline -5` and amend before pushing).
Then move the closed block to the **Recently closed** section at the bottom of `docs/ISSUES.md`, following the format used by the other DONE entries (e.g. #84, #85, #87).
Append a one-line resolution paragraph immediately under the **Commits:** line:
```markdown
**Resolution (2026-05-25 · #100):** Replaced the cell-level
`hiddenTerrainCells` mechanism with retail's per-vertex Z nudge
(`zFightTerrainAdjust = 0.00999999978`) applied inside the modern
terrain vertex shader. Render terrain everywhere; coplanar building
floors win the depth test by being 1 cm higher than the rendered
terrain. Physics path untouched. ~50 LOC of `BuildingTerrainCells`
plumbing removed across LandblockMesh / LoadedLandblock /
LandblockLoader / GameWindow / GpuWorldState / LandblockStreamer
plus the corresponding unit test. Retail anchors:
acclient_2013_pseudo_c.txt:1120769 + :702254.
```
- [ ] **Step 2.11: Commit**
```powershell
git add src/AcDream.Core/Terrain/LandblockMesh.cs `
src/AcDream.Core/World/LoadedLandblock.cs `
src/AcDream.Core/World/LandblockLoader.cs `
src/AcDream.App/Rendering/GameWindow.cs `
src/AcDream.App/Streaming/GpuWorldState.cs `
src/AcDream.App/Streaming/LandblockStreamer.cs `
tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs `
docs/ISSUES.md
git commit -m @'
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)
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>
'@
```
After committing, fix up the SHA references in `docs/ISSUES.md` if Step 2.10 used placeholders:
```powershell
git log --oneline -3
```
Capture the two SHAs (Task 1 and Task 2). Edit `docs/ISSUES.md` to replace `<TASK_1_COMMIT_SHA>` and `<TASK_2_COMMIT_SHA>` with the real values, then amend:
```powershell
git add docs/ISSUES.md
git commit --amend --no-edit
```
---
## Task 3: Visual verification at Holtburg
**This is the acceptance test.** The M1.5 milestone explicitly states visual verification is the acceptance gate. The two unit tests we have (`dotnet build` and `dotnet test`) prove the code compiles and the focused suites still pass — they don't prove the bug is gone.
- [ ] **Step 3.1: Launch the client against the live ACE server**
Per CLAUDE.md "Running the client against the live server" — graceful-close any prior session first.
```powershell
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($proc) {
$proc.CloseMainWindow() | Out-Null
if (-not $proc.WaitForExit(5000)) { $proc | Stop-Process -Force }
}
Start-Sleep -Seconds 3
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
Tee-Object -FilePath "issue100-verify-launch.log"
```
Launch in the background; give the window ~8 seconds to reach in-world state.
- [ ] **Step 3.2: Inspect each acceptance scenario**
| Scenario | Expected outcome | Pass/Fail |
|---|---|---|
| Stand outside Holtburg cottage (any cottage) | Ground around the building reads as continuous cobblestone / grass — no dark rectangular patch | |
| Walk in a circle around a cottage | Terrain stays continuous on all four sides | |
| Approach the inn from the south | No transparent rectangle in front of the inn | |
| Approach the inn from the north | No transparent rectangle behind the inn | |
| Building floors at threshold height | No Z-fighting flicker between terrain and building floor | |
| Walk inside the cottage cellar | (Regression check) Z-fight inside / floor still walkable | |
Each row must read PASS. If any row reads FAIL — stop, capture a screenshot, file as a follow-up, and ASK before "fixing" anything new.
- [ ] **Step 3.3: Close the client cleanly**
```powershell
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($proc) {
$proc.CloseMainWindow() | Out-Null
if (-not $proc.WaitForExit(5000)) { $proc | Stop-Process -Force }
}
```
- [ ] **Step 3.4: User sign-off**
This is the *user's* acceptance gate. After Step 3.2 produces all-PASS, report to the user with concrete observations from at least 3 cottages + the inn, then await the user's explicit confirmation before declaring #100 closed.
If the user observes any new visual regression (e.g. terrain visibly sinking into water polygons, or scenery objects appearing to float), pause and investigate — that's a sign the 0.01 nudge interacts with something we haven't anticipated. Do not retry or paper over.
---
## Self-review (post-write)
**Spec coverage:**
| Requirement | Task | Coverage |
|---|---|---|
| Add 1 cm Z subtract in terrain vertex shader at line 139 | Task 1 | ✔ |
| Reference retail anchors in code comment | Task 1.1 | ✔ |
| Delete `hiddenTerrainCells` parameter + collapse block | Task 2.1 | ✔ |
| Delete `BuildingTerrainCells` field on `LoadedLandblock` | Task 2.2 | ✔ |
| Delete `BuildBuildingTerrainCells` method + `Load` call | Task 2.3 | ✔ |
| Update all `LandblockMesh.Build` call sites | Task 2.4 | ✔ (3 sites in GameWindow.cs) |
| Update all `LoadedLandblock` ctor sites | Tasks 2.4, 2.5, 2.6 | ✔ (1 in GameWindow.cs + 6 in GpuWorldState.cs + 1 in LandblockStreamer.cs) |
| Delete dead unit test | Task 2.7 | ✔ |
| Sweep for stragglers | Task 2.8 | ✔ |
| Close issue #100 in ISSUES.md | Task 2.10 | ✔ |
| Visual verification at Holtburg cottages | Task 3.2 | ✔ |
| Don't touch physics | Constraint 1, Task 1.1 comment | ✔ |
| Don't use glPolygonOffset | Constraint 2 | ✔ |
| Don't keep both mechanisms | Constraint 3 | ✔ |
**Placeholder scan:** Done — no "TBD", "TODO", "implement later", or "similar to Task N" references. Every step has the actual code.
**Type consistency:** `LandblockMesh.Build` signature appears in Tasks 2.1 + 2.4; both drop the 7th parameter consistently. `LoadedLandblock` ctor appears in Tasks 2.2 + 2.4 + 2.5 + 2.6; all use the 3-argument form. `BuildingTerrainCells` field referenced in Tasks 2.4 + 2.5 + 2.6 + 2.8; all removals consistent.
**Spec compliance check:** plan matches the user's session brief structure (3 tasks ≈ "expect 34 tasks" — the original brief's Tasks 2 + 3 are merged here because the test deletion has a compile dependency on the plumbing removal). Visual verification is the explicit acceptance test, matching the brief's "visual verification is the acceptance test" sentence. Do-not-retry items from the handoff doc are honored as Constraints + do-not-fix language in Step 3.4.