merge: bring main into claude/hopeful-maxwell-214a12 (LayoutDesc importer branch)

main was 65 commits ahead of this branch's fork point. Only conflict was the
divergence register: both sides appended an 'AP-32' row. Resolved by keeping
main's AP-32..AP-36 (cell-shell lift, look-in cells, alpha deferral, dungeon
streaming, point lights) and renumbering the importer's row to AP-37; AP header
count -> 37. GameWindow.cs auto-merged cleanly. Verified: AcDream.App builds
0/0; AcDream.App.Tests 354 passed / 1 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-15 16:19:15 +02:00
commit 5ac9d8c19c
53 changed files with 6691 additions and 439 deletions

View file

@ -0,0 +1,95 @@
# Night-session handoff (2026-06-12): nine closes shipped; NEXT = #108-residual
**Branch state:** `claude/thirsty-goldberg-51bb9b`, pushed to BOTH remotes at
`49cffe6`. Suites green at every commit: App 261+1skip / Core 1439+2skips /
UI 420 / Net 294. CLAUDE.md "Current state" + the render digest
(`claude-memory/project_render_pipeline_digest.md`) are refreshed to this
truth — orient there first.
## 1. What this session closed (all user-gated; do NOT re-litigate)
| Closed | Root cause | Commits |
|---|---|---|
| **#130** doorway top-edge strip | TWO stacked causes: scissor box `Floor(origin)+Ceiling(size)` under-covers top/right (sub-pixel, `NdcScissorRect`); THE strip = the +0.02 m shell draw-lift missing from draw-space portal consumers post-f35cb8b (6.7 px @2.4 m, measured) | `6c4b6d6`, `5135066` (AP-32 row added) |
| **#129** doors leak through terrain at ~a landblock | constant 0.0005 NDC punch bias spans ~190 m of eye depth at distance; capped to 0.5 m eye-space (`MarkBiasNdc`) | `4ba7148` (AD-18 updated) |
| **#113** hill-cottage phantom stairs | dead via `2163308` (cache cross-serving) — re-gate confirmed | — |
| **#124** far-building back walls through openings | interior-root look-ins ported as a LANDSCAPE-STAGE sub-pass (decomp: LScape::draw runs FIRST in DrawCells' outside branch, pc:432719, pre-clear/pre-seal; seeds clip vs the INSTALLED view → `BuildFromExterior(seedRegion:)`; punch-all-then-draw). NEVER merge look-ins into the main frame (post-clear seal z-kill) | `77cef4c` (AP-33 added) |
| **#132** candle flame vs through-opening background | slice particles drew BEFORE the look-ins / merged interiors (no depth self-protection) — the FlushAlphaList deferral ported as the two-phase slice split + outdoor post-frame attached pass | `20d1730`, `87afbc0` (AP-34 added) |
| **#131** portal swirl missing through doorways | FOUR layers (see lesson below); final: the portal is a SERVER object inside the hall's PORCH cell (look-in cell) → partition.Dynamics → dynamics-last culls it (no look-in cells in the main cone) + post-seal z-fail. Fix: `DrawBuildingLookIns` draws look-in-cell dynamics + emitters (retail nested DrawCells/`DrawObjCellForDummies`) | `1d3f9a8`, `47f32cd`, `d208002` |
| **UN-2** GetMaxSpeed ×4 contradiction | the implementation was retail-correct; BN pseudo-C drops x87 fmuls — byte-verified (3× `fmul [0x7C8918]`=4.0f); doc rewritten, weenie-null default aligned to literal 1.0; row deleted | `0cb97aa` (verifier `tools/verify_un2_fmul.py`) |
## 2. THE #131 LESSON (cost: 4 fix iterations)
**Identify the ENTITY before theorizing about draw passes.** Three
real-but-adjacent fixes shipped before the elimination chain (teleport pCell
flip → owner cell; headless replay → flood admits it; partition routing →
exactly one possible drop site) forced the answer. Two tools that would have
shortened it to one iteration:
- **The pick line**: left-click prints `[B.4b] pick guid=… name=…` +
`[B.7] pick-info … setup=…` — names any clickable object in the log.
- **The teleport/pCell flip**: walking onto/into a thing prints its cell.
Both need zero new code. The register also already KNEW the answer (AP-33's
"look-in DYNAMICS are not drawn — deferred") — scan-the-register-on-symptom
applies to rows YOU wrote hours earlier.
## 3. NEXT (the queue to the M1.5 → M2 boundary)
1. **#108-residual — cellar-ascent grass window (NEXT, desk-first).**
Climbing out of a cellar, grass covers the exit door until the eye pops
above grade. Punch/seal exonerated; it is MEMBERSHIP/VIEWER-side (which
cell the camera resolves while the eye is below grade). Apparatus
designed: a VERTICAL exit-walk-harness variant (HouseExitWalkReplayTests
machinery driving the camera up cellar stairs, watching viewer-cell
resolution per step). Read the physics digest + ISSUES #108 before
starting. User needed only for the final cellar gate.
2. **#127 — distant-building admission churn** (flood size oscillates ±13
cells at mm eye deltas; suspect list includes the PortalBounds frustum
pre-gate — machinery #124 now reuses for interior roots).
3. **#116 — slide-response family** (physics, oracle-first: one cdb session).
4. **#125 sticky-drop debt** — failed texture uploads never retried
(session-sticky invisible meshes); robustness, no visual gate.
## 4. Apparatus added this session (all env-gated, kept)
| Tool | How | For |
|---|---|---|
| `[outstage]`/`[outstage-pt]`/`[outstage-own]` | `ACDREAM_PROBE_OUTSTAGE=1` (+`ACDREAM_DUMP_ENTITY=<ids>` doubles as the owner watchlist) | outside-stage dynamics routing/cone verdicts; scene-particle owner matching |
| `Issue130DoorwayStripTests` | App.Tests | aperture-vs-gate top-edge gap in DRAWN (lifted) space; the lift-seam sensitivity pin |
| `NdcScissorRectTests` / `Issue129PunchBiasTests` | App.Tests | scissor containment; punch-bias eye-span cap |
| `Issue124LookInSeedRegionTests` | App.Tests | seedRegion semantics at the real corner-building door |
| `Issue131SetupProbeTests` | App.Tests | dat setup dumps + the porch-admission replay of a captured frame |
| `tools/verify_un2_fmul.py` | `py` | re-derive the GetMaxSpeed ×4.0 byte proof |
## 5. Paste-ready pickup prompt
```
Pick up acdream as a SENIOR 3D ENGINE DEVELOPER on #108-residual (the
cellar-ascent grass window). Branch claude/thirsty-goldberg-51bb9b ==
pushed both remotes at 49cffe6. Read FIRST: CLAUDE.md "Current state",
docs/research/2026-06-12-night-session-handoff-108-residual-next.md (THE
handoff), then BOTH digests (render + physics; DO-NOT-RETRY tables apply).
WORK ORDER:
1. #108-residual — eye-below-grade membership at cellar exits. Build the
VERTICAL exit-walk harness variant (HouseExitWalkReplayTests machinery,
a cellar staircase fixture), watch viewer-cell resolution per step while
the eye is below terrain grade; pin where the resolver demotes to
outdoor/terrain. Punch/seal are exonerated — do NOT touch them.
2. Then #127 (admission churn; PortalBounds pre-gate suspect), #116
(slide-response, oracle-first cdb), #125 sticky-drop debt.
3. When the ledger clears: run the M1.5 DUNGEON DEMO as the milestone
exit gate (milestones doc: enter any dungeon via portal, 3-5 rooms,
walls block / stairs work / lighting correct / transitions smooth).
The old blocker #95 died with the Option A rewrite (the ACME BFS it
lived in was deleted in T4); the portal entry flow is field-tested
(the 2026-06-12 accidental teleport). Dungeon-specific findings
(likely A7 lighting items) get fixed inside M1.5; a clean demo lands
M1.5 -> update the milestones doc + CLAUDE.md and start M2 (kill a
drudge; first port target per the research memos: CombatMath).
The user's reports are AXIOMS. Visual gates are the acceptance tests.
Suites green per commit: App 261+1skip / Core 1439+2skip / UI 420 /
Net 294. Register discipline: new deviation = same-commit row. For any
object-specific render bug: IDENTIFY THE ENTITY FIRST (the pick line
[B.4b] names clicked objects; pCell flips name cells) — the #131 lesson.
```

View file

@ -0,0 +1,205 @@
# Handoff (2026-06-13): M1.5 EXTENDED — dungeon support (full Phase G.3). Design grounded; ready to brainstorm → spec → implement.
**Branch:** `claude/thirsty-goldberg-51bb9b`, pushed to BOTH remotes at the
HEAD this doc commits with. Suites green: App 264+1skip / Core 1445+2skip /
UI 420 / Net 294 (the dungeon dat-probe test added this session is
output-only).
This session closed a batch of M1.5 render/physics issues, then — at the
dungeon-demo gate — discovered dungeons don't work and the user **extended
M1.5 to include full dungeon support (Phase G.3)**. M2 is re-deferred. The
design is grounded (5-way reference research + a decisive dat probe); the
next session brainstorms approaches → writes the spec → implements.
---
## 1. What this session shipped (all on branch, pushed, most user-gated)
| Item | Outcome | Commits |
|---|---|---|
| **#108-residual** (cellar grass window) | CLOSED, user-gated "Yes it is fixed." Terrain was drawn DOUBLE-SIDED; the grass was the grade sheet's underside seen from a below-grade cellar eye. Ported retail `landPolysDraw` eye-side gate as terrain backface cull. Membership/viewer EXONERATED by a vertical cellar-ascent harness. | `007af13`, `96a425a`, `bf80067` |
| **#127** (distant-building flood flap) | CLOSED, user-gated "Seems to have been fixed." Died with the W=0 clip port (`987313a`); confirmed by a run-past churn detector (0 churn, 21 buildings × 5 distances × 100 mm-steps). | `4ad6fb9` |
| **#125** (sticky-drop debt) | CLOSED. Bounded upload retry — a failed `UploadMeshData` re-stages for the next frame up to `MaxUploadRetries` (counter on the `ObjectMeshData`); the CPU-cache short-circuit no longer permanently strands a failed upload. | `8682a8d` |
| **#116** (slide-response) | PARTIAL. Ghidra (the user pointed me to the running Ghidra MCP) resolved the BN `test ah,5` branch-sign ambiguity: `slide_sphere` compares squared magnitudes against `F_EPSILON` (0.0002), not `EpsilonSq` (4e-8) — fixed `TransitionTypes.cs:3098,3105`, full physics suite green. The two reported shapes still need a cdb trace (shape-1 = upstream collision-normal recording; shape-2 = D4 first-frame dispatch). | `35961f2`, `bf18a54` |
---
## 2. The milestone churn (read this — the docs were corrected)
- I briefly marked **M1.5 LANDED** on the building/cellar demo and started M2
(`1bf037a`). **The user reverted that:** the indoor world isn't done while
dungeons are broken, so M1.5 is EXTENDED to include dungeon support, and the
user chose the **FULL Phase G.3 scope** (streaming + portal-space loading
screen + `PlayerTeleport` handling). Correction committed `9c2ceb2`.
- **Current truth:** M1.5 ACTIVE; building/cellar demo DONE + user-gated;
dungeon support (G.3) is the remaining M1.5 exit-gate. M2 (CombatMath first
port) DEFERRED. Docs reflect this (milestones doc, CLAUDE.md current-state,
ISSUES.md #133).
---
## 3. The dungeon bug — CORRECTED root cause (issue #133)
User attempted the dungeon demo via the **meeting-hall portal** → "no dungeon,
just ocean." ACE logged a flood of `failed transition for +Acdream from
0x01250126 [30 -60 6.0] to 0xA9B0000E [-32227 -26748 5.9]` … marching south at
Z≈0.9 (underwater).
**Diagnostic capture (`launch-dungeon-diag.log`, probes
`ACDREAM_PROBE_CELL`/`ACDREAM_PROBE_VIEWER`/`ACDREAM_WB_DIAG`):**
```
live: teleport arrival — old lb=(169,180) new lb=(1,37) dist=42524.0
[snap] claim=0xA9B3000E pos=(30,-60,6.005) cells=17 bestCell=0xA9B30103 ... indoor=False -> targetCell=0xA9B3000E
live: teleport complete — snapped to <30,-60,6.005> cell=0xA9B3000E
[cell-transit] A9B3000E -> A9B2000E -> A9B1000E -> ... (sliding south into ocean)
```
ACE correctly placed the player in dungeon cell **0x01250126** (landblock
`0x0125` = (1,37)). acdream's arrival handler (`GameWindow.cs:4908-4931`)
recenters streaming to (1,37) but then **immediately** calls
`_physicsEngine.Resolve(pos=(30,-60,6.005), cell=0x01250126)` to snap the
player — **before the dungeon landblock has streamed in**. Resolve can't find
the dungeon cell, falls back to an outdoor scan against the **still-resident
Holtburg landblocks**, and snaps to `0xA9B3000E` (Holtburg's south edge, local
(30,60) maps into the block south of the A9B4 spawn). Streaming then shifts
the frame out from under the player → slides south into ocean.
### ⚠️ The "terrain-less landblock" framing is WRONG (verified by dat probe)
A pipeline-seam research agent *assumed* dungeon landblocks have no `LandBlock`
record (so `LandblockLoader.Load` returns null) and produced a 13-seam
"rewrite the pipeline for terrain-less landblocks" plan. **A direct dat probe
(`DungeonLandblockDatProbeTests`, committed) refutes that:**
```
0x0125 (dungeon): LandBlock 0x0125FFFF PRESENT, Height[81] allZero=True (flat)
LandBlockInfo: NumCells=71, Buildings=0, Objects=0
EnvCells 0x0100.. present (the 71 dungeon rooms)
0xA9B4 (Holtburg): LandBlock PRESENT, heights non-zero; NumCells=123, Buildings=12, Objects=114
```
A dungeon landblock is a **flat-terrain landblock** (all-zero height index =
the lowest/"ocean" terrain) **plus its EnvCells, no buildings/objects**. So
`LandblockLoader.Load(0x0125…)` returns a valid flat landblock, the terrain
mesh builds a flat plane, and `PhysicsEngine.AddLandblock` gets a valid flat
`TerrainSurface`. **The existing pipeline can already stream a dungeon
landblock.** The 13 terrain-dependency seams are NOT the blocker.
**The real blocker is narrow: teleport TIMING + PLACEMENT.**
---
## 4. Reference grounding (5-way research; dat agent failed, replaced by the probe above)
**holtburger (client-behavior oracle):**
- PlayerTeleport (0xF751) → enter `EnteringWorld` (portal space), **suspend
physics bodies**, send **LoginComplete immediately** (no waiting for assets).
- Exit portal space → `InWorld` when the server sends ObjectCreate (entities) +
UpdatePosition (player) + the **StartGame** event → resume bodies.
- holtburger does NOT stream landblocks (entity-centric); not our model — we
DO stream from our own dats. Take the **FSM shape** (EnteringWorld/InWorld +
suspend/resume) not the no-streaming part.
- DDD is NOT part of the teleport flow (responds empty). (`messages.rs:480-486`,
`:190-195`, `player.rs:71-79`, `types.rs:169-175`.)
**ACE (server):** `Player_Location.cs:654-707` Teleport() sends PlayerTeleport
(sequence) → a **fake UpdatePosition** to trigger client load → the real
UpdatePosition with PositionPack (CellID = dungeonID<<16 | cellIndex, e.g.
`0x01250126`, xyz, rotation). **Server sends NO geometry — client loads cells
from its own dats by cellID** (matches our dat-driven model). Portal:
`Portal.cs:269-292` ActOnUse → AdjustDungeon (corrects cell id) →
ThreadSafeTeleport. **Dungeons are SINGLE-landblock** (`Player_Tick.cs:548-560`
forbids moving between dungeon landblocks without teleport) → "multi-landblock
LOD" in the full-G.3 scope is MOOT for AC dungeons. IsDungeon = all heights 0 +
NumCells>0 + no buildings (`Landblock.cs:575-631`).
**Retail decomp (client):** terrain (`CLandBlock::grab_visible_cells`) and
dungeon cells (`CEnvCell::grab_visible_cells`, :311878) load on **separate
paths**; a cell with `seen_outside==0` loads ZERO terrain and walks only its
`stab_list` (adjacent EnvCells). **Portal-space = a 6-state `TeleportAnimState`
FSM** (:219682-219774): WORLD_FADE_OUT → TUNNEL_FADE_IN → TUNNEL (hold while
loading) → TUNNEL_FADE_OUT → WORLD_FADE_IN → OFF; `m_pPortalSpace` is the
tunnel viewport (the "loading"/black screen). Retail gates cell-ready on DDD
(server cell push) — **we don't need DDD** (we have the dats); we gate on our
own streaming hydration. Open: no distinct "pink screen" asset found — retail's
loading visual is the portal tunnel.
**acdream pipeline seams (corrected by the dat probe):** the dungeon landblock
streams fine as flat-terrain. Real seams that matter:
- `GameWindow.cs:4908-4931` — teleport arrival: recenter then **Resolve
immediately** (the bug). No hold-until-hydration.
- `PhysicsEngine.IsSpawnCellReady` (`:468`) — the EXISTING #107 gate; already
handles indoor cells (checks DataCache for 0x0100+). **Reuse it for the
teleport-arrival path.**
- EnvCell hydration (render `_cellVisibility`/`EnvCellRenderer`; physics
`CacheCellStruct`) is iterated from `LandBlockInfo.NumCells` and is
**orthogonal to terrain** — should fire for a dungeon landblock once it
streams (`GameWindow.cs:5564-5576`, `6015-6028`). VERIFY it does.
- Placement: the player is at cell `0x01250126`, pos (30,60,6.005); must be
placed in the **EnvCell** (the #107/#111 validated-claim path,
`WalkableFloorZNearest`), not on the flat terrain.
---
## 5. Design direction (to confirm in the brainstorm)
A retail-faithful, much-narrower-than-feared shape:
1. **Teleport state machine (portal space).** On PlayerTeleport: enter a
PortalSpace/EnteringWorld state, suspend player physics, (optionally) start
the retail `TeleportAnimState` tunnel FSM for the loading visual. On arrival
UpdatePosition: recenter streaming on the destination landblock, then **HOLD**
— do not snap — until the destination landblock + the claimed EnvCell hydrate
(reuse `IsSpawnCellReady`). Then place into the EnvCell (validated-claim
path), exit PortalSpace → InWorld, resume physics, send LoginComplete.
(acdream already has `OnTeleportStarted`/portal-space + the #107 hold for
LOGIN — extend that machinery to the teleport-arrival path rather than
snapping at `:4928`.)
2. **Streaming a far teleport.** Confirm the recenter actually drives the
streamer to load the destination dungeon landblock (the Chebyshev window
around the new center) and unloads the old neighborhood without stranding the
player. The dungeon streams as a flat-terrain landblock — no new loader path
needed, but verify the apply path + EnvCell hydration fire.
3. **Render/physics in the dungeon.** Once the EnvCells hydrate, the existing
PView indoor render + per-cell collision should work (same as buildings).
The flat terrain renders below; PView roots at the viewer EnvCell. VERIFY the
3-5-room navigation, walls block, stairs, lighting (A7 not done — expect
lighting findings), transitions.
4. **Portal-space loading screen (full-G.3 polish).** The retail 6-state tunnel
FSM (`TeleportAnimState`) — implement after the core teleport+place works, or
a simpler fade-to-black first.
**Open design questions for the brainstorm:**
- Do we implement the retail `TeleportAnimState` tunnel FSM faithfully now, or a
simpler fade-to-black for M1.5 and the full tunnel later?
- How long to HOLD before giving up (the dungeon may need several frames to
stream); what's the failure/timeout behavior?
- Does the existing streaming controller already load a landblock 42 km away on
recenter, or does it assume incremental movement? (Confirm the recenter→load
path for a big jump.)
- Placement: the cell-local pos (30,60,6.005) vs the EnvCell origins (~(0,30,0))
— confirm the EnvCell BSP contains the point and the #107/#111 walkable-floor
placement lands the player on the dungeon floor.
---
## 6. Apparatus added this session (kept)
| Tool | How | For |
|---|---|---|
| `DungeonLandblockDatProbeTests` | `dotnet test --filter DungeonLandblockDatProbe` | Dumps the dat structure of a dungeon (0x0125) vs outdoor (A9B4) landblock — the terrain-less-vs-flat resolution |
| `launch-dungeon-diag.log` | `ACDREAM_PROBE_CELL=1 ACDREAM_PROBE_VIEWER=1 ACDREAM_WB_DIAG=1` | The teleport→snap→slide capture; `[snap]`/`[cell-transit]`/`live: teleport` lines are the chain |
| `Issue108CellarAscentViewerReplayTests` | App.Tests filter | Vertical cellar-ascent viewer harness (membership EXONERATED for #108) |
| `Issue127FloodFlipReplayTests.DistantBuildingStrafe_NoAdmissionChurn` | App.Tests filter | #127 run-past churn-detector regression pin |
Decomp grounding: holtburger teleport flow, ACE Teleport/Portal/AdjustDungeon,
retail `CEnvCell::grab_visible_cells` (:311878) + `TeleportAnimState` FSM
(:219682-219774). Full raw research in the workflow output (this session).
---
## 7. Next session: brainstorm → spec → implement
The brainstorming skill was invoked and scope was set (full G.3). The next
session resumes the brainstorm at "propose 2-3 approaches" with the grounding
above, settles the design, writes the spec to
`docs/superpowers/specs/2026-06-13-dungeon-support-design.md`, then →
writing-plans → implement. The paste-ready pickup prompt is in the session
message that produced this doc.