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