# Phase G.3 — Dungeon Support (Design Spec) > **Status:** APPROVED design (brainstorm 2026-06-13). Next: `writing-plans`. > **Milestone:** M1.5 ("Indoor world feels right"). G.3 is the remaining M1.5 > exit-gate. M2 (CombatMath) stays deferred until this lands. > **Issue:** [#133](../../ISSUES.md) (teleport-into-dungeon snaps to ocean) + > [#95](../../ISSUES.md) (dungeon portal-graph visibility blowup — re-assessed > below). > **Supersedes** the §12 port-plan of > [`docs/research/deepdives/r09-dungeon-portal-space.md`](../../research/deepdives/r09-dungeon-portal-space.md): > most of R9's "new types" (EnvCell loader/renderer/physics, PortalVisibility > BFS, multi-cell transit) already shipped and power the building/cellar demo. > r09 stays the **retail contract reference** for the wire formats, the > EnvCell/CellPortal layout, and the recall taxonomy. --- ## 0. TL;DR Dungeons don't work because of **one timing+placement gap on one code path**, not a terrain-less-pipeline rewrite. A dungeon landblock (e.g. `0x0125`, the Holtburg-area meeting hall) is a **flat-terrain** landblock (`LandBlock` present, all-zero heights) + 71 EnvCells + no buildings — it already streams, renders, and collides through the existing pipeline. The teleport-arrival handler snaps the player **before** that landblock has streamed in, so Resolve falls back to the resident Holtburg blocks and lands the player in ocean. The fix is retail's own shape: **hold the player in portal space until the destination cell is hydrated, then place into the EnvCell** — reusing the #107/#111 login machinery — and then layer retail's portal-tunnel visual (`TeleportAnimState`) on top. We ship it in four installments, gated by one visual acceptance test. --- ## 1. Corrected root cause (verified) ### 1.1 The "terrain-less landblock" framing is WRONG (dat-verified) A prior research pass assumed dungeon landblocks have no `LandBlock` record, so `LandblockLoader.Load` returns null and the whole streaming/render/physics pipeline needs terrain-less support. **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** (lowest/"ocean" terrain height index) **plus its EnvCells, no buildings/objects**. `LandblockLoader.Load` returns a valid flat landblock; the terrain mesh builds a flat plane; `PhysicsEngine.AddLandblock` gets a valid flat `TerrainSurface`. **The existing pipeline already streams a dungeon landblock.** This matches ACE's `IsDungeon` (all heights 0 + `NumCells > 0` + no buildings — `Landblock.cs:575`) and the single-landblock rule (`Player_Tick.cs:548-560` forbids moving between dungeon landblocks without a teleport — so "multi-landblock dungeon LOD" is moot). ### 1.2 The real blocker: teleport TIMING + PLACEMENT `OnLivePositionUpdated` (`src/AcDream.App/Rendering/GameWindow.cs:4877-4961`) detects teleport arrival as **any** player position update while in PortalSpace (correct, per #107), then **unconditionally**: 1. Recenters streaming to the destination landblock (`_liveCenterX/Y`, `:4908-4925`). 2. **Immediately** calls `_physicsEngine.Resolve(destPos, destCell, …)` to snap the player (`:4927-4931`) — **before the destination landblock has streamed in**. 3. Snaps entity + controller (`:4935-4939`), exits PortalSpace (`:4950`), sends `LoginComplete` (`:4953-4959`). Because the dungeon landblock isn't resident yet, Resolve can't find the destination 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 → they slide south into ocean. ACE logs the matching `failed transition for +Acdream from 0x01250126 … to 0xA9B0000E …` chain (captured in `launch-dungeon-diag.log`). **There is no hold-until-hydration on the teleport-arrival path.** The #107 *login* path directly above it (`GameWindow.cs:1010-1024`) HAS exactly this gate; the teleport path doesn't. --- ## 2. Grounded seam facts (the design rests on these) All five verified against current code this session (high confidence). ### 2.1 Teleport-arrival + PortalSpace FSM - `OnTeleportStarted` (`GameWindow.cs:~4971-4976`) — on `PlayerTeleport (0xF751)` sets `_playerController.State = PlayerState.PortalSpace`, freezing movement. - `PlayerMovementController.Update` (`PlayerMovementController.cs:840-854`) returns a zero-movement result while `State == PortalSpace` — **PortalSpace already doubles as the input-freeze.** It can equally serve as the hydration-wait gate. - Exit is **only** via the arrival detection in `OnLivePositionUpdated` (`:4880`). No timeout, no cell-hydration gate today. ### 2.2 #107/#111 login machinery (directly reusable) - `PhysicsEngine.IsSpawnCellReady(cellId)` (`PhysicsEngine.cs:468-472`): outdoor (`cellId & 0xFFFF < 0x0100`) → always ready; indoor → `DataCache.GetCellStruct(cellId) is not null` (the cell's physics BSP has hydrated). - `IsSpawnClaimUnhydratable(claim)` (`GameWindow.cs:11728-11748`): fetches the dat `LandBlockInfo` at `(lb & 0xFFFF0000) | 0xFFFE`; a claim whose low word is `>= 0x0100 + NumCells` (or `NumCells==0`) can **never** hydrate → reject fast (distinguishes a bogus claim from a not-yet-streamed one). - #107 login hold (`GameWindow.cs:1010-1024`): `isSpawnGroundReady` waits for terrain AND (claim outdoor OR `IsSpawnCellReady` OR `IsSpawnClaimUnhydratable`). No timeout today (login can afford to wait forever; teleport cannot — see §5). - #111 validated-claim placement (`PhysicsEngine.cs:626-646`): when `snapDiag (zero-delta) && adjustedFound && indoor`, place via `WalkableFloorZNearest` (`:383-406`) — projects Z onto the claim cell's **own physics walkable polygons** (`normal.Z >= PhysicsGlobals.FloorZ`, 0.6642), cell-local, nearest to the reference Z. Returns `null` if the cell isn't hydrated → falls through to the legacy `bestCell` scan (**the ocean bug**). - **The teleport-arrival Resolve call is already the same shape as login entry.** The gate only needs to sit in front of it; no change to Resolve or WalkableFloorZNearest. (Both already key on the full prefixed cell id + indoor/outdoor.) ### 2.3 Streaming far recenter (works as-is) - `StreamingRegion.RecenterTo` (`StreamingRegion.cs:180-283`) recomputes the near/far Chebyshev window **from scratch** around the new center — a 42 km jump is treated identically to a 1-step move. No incremental-movement assumption. - Drain: `StreamingController` applies ≤ `MaxCompletionsPerFrame` (default 4) results/frame; `ApplyLoadedTerrainLocked` (`GameWindow.cs:5941-6150`) does GPU upload + cell-visibility registration + AABB + `PhysicsEngine.AddLandblock` + EnvCell/portal registration. Estimate: **~7-8 frames (~120-130 ms)** to hydrate a 5×5 near window; physics ready +1-2 frames. - Recenter keeps the old neighborhood until hysteresis unload (NearRadius+2 demote, FarRadius+2 unload), so the player isn't instantly stranded. - **New code needed:** reuse the #107 login-gate **terrain-ready signal** `_physicsEngine.SampleTerrainZ(x,y) is not null` (non-null once the destination terrain landblock has applied) — no separate "landblock applied" query is required. Plus dest-coord validation (reject out-of-world coords — a malformed portal dest would otherwise leave the player in an invisible, unloadable landblock). ### 2.4 EnvCell hydration coupling (latent landmine — decouple) - In `BuildInteriorEntitiesForStreaming` (`GameWindow.cs:5564-5651`), both `BuildLoadedCell` (the portal-visibility node) **and** `_physicsDataCache.CacheCellStruct` (the physics BSP) sit **inside** the render guard `if (cellSubMeshes.Count > 0)` (`:5602`). A cell whose render mesh is empty (`CellMesh.Build` returns nothing — e.g. all-untextured/`Stippling.NoPos` polys) silently gets **no visibility node and no collision**, even if it has walkable physics polygons. `CellTransit.FindTransitCellsSphere` then `GetCellStruct → null → continue` (silently skips it) → fall-through-floor. - A normal dungeon *room* has textured walls → non-empty submeshes → the guard passes, so this is **probably not the meeting-hall blocker** — but it is a real correctness landmine for any geometry-less collision cell, and decoupling is cheap and retail-correct (physics/visibility do not depend on visible geometry). **Fix:** gate `CacheCellStruct` on `cellStruct.PhysicsBSP != null` and `BuildLoadedCell` on `cellStruct != null`, independent of the render submesh count. (`CacheCellStruct` already early-returns on null BSP internally — `PhysicsDataCache.cs:172` — so moving it out is safe.) ### 2.5 #95 — dungeon portal-graph visibility blowup (RE-ASSESSED: likely superseded) - ISSUES.md #95 (`888-913`): on a 2026-05-21 **A6.P1 scen5 (Town Network hub)** trace, `visibleCells` per cell exploded to 135-145 with spurious cells from landblocks `0x020A`/`0x0408` (other dungeons). Its "Files" point at the WB `EnvCellRenderManager`/`VisibilityManager` + the Streaming cell-cache. - **That code path was DELETED by the T1-T6 render rewrite (2026-06-11)** (T4: "per-frame ACME BFS deleted… InteriorRenderer/DrawPortal deleted"). The current flood, `PortalVisibilityBuilder.Build`, (a) confines neighbors to the camera cell's landblock (`lbMask = cameraCell.CellId & 0xFFFF0000`, `:131`) and (b) has **enqueue-once termination** (`queued` HashSet, `:165` — "at most N cells are ever processed"). Since AC dungeons are single-landblock, that confinement is *correct*, and the cross-landblock 135-cell blowup **structurally cannot reproduce**: a single-landblock flood visits ≤ `NumCells` distinct cells (71 for the meeting hall). - **Verdict (pre-gate, 2026-06-13 AM):** #95's evidence is stale, from a deleted path; the current pipeline looked bounded. Treated #95 as likely superseded. - **⚠️ GATE CORRECTION (2026-06-13 PM — #95 is CONFIRMED LIVE):** the G.3a visual gate ran a real `PlayerTeleport` into the `0x0007` dungeon (Town Network). The core hold+place worked (player grounded on the dungeon floor, z=0 — no ocean), but **WB-DIAG exploded to entSeen=6.5M / instances=9.1M / drawsIssued=590K per frame** (vs. 3345 / 4667 at Holtburg), with a flood of `[mesh-miss] 0x000100xxxx` interior re-requests → the dungeon renders as "thin air." **#95 reproduces under the current Option-A pipeline.** The "bounded flood" reasoning was wrong for the `0x0007` dungeon (the grounding agent's "still live" verdict was correct; this doc over-discounted it). **G.3b is now REQUIRED, not conditional** (§3.2). The retail-faithful fix shape stands: port `CEnvCell::grab_visible_cells` (:311878) stab_list bounding — a `seen_outside==0` cell walks ONLY its `stab_list`. --- ## 3. The plan (Approach C — phased full-G.3) Each installment lands a **complete retail behavior** (the BR-2 half-port lesson). The visual gate sits as early as possible, right after the core. ### 3.1 G.3a — Core teleport-into-dungeon (the blocker) **Goal:** teleporting into the meeting-hall dungeon lands the player standing in the dungeon cell, on the floor, with walls blocking — no ocean, no ACE `failed transition` spam. **New component — `TeleportArrivalController`** (`src/AcDream.App/World/`): - Owns a small phase: `Idle / Holding / Placing`, plus `_pendingArrival` `(destPos, destCellId, deadline)`. - Lives outside `GameWindow` (Code Structure Rule 1: no new feature bodies in the god-object). `GameWindow.OnLivePositionUpdated` hands the arrival to it and calls its per-frame `Tick`; `GameWindow` keeps only the wiring. - Unit-testable in isolation (no GL, fake readiness predicate + fake Resolve). **Control flow (replaces the unconditional snap at `GameWindow.cs:4927-4950`):** 1. On arrival update in PortalSpace: validate `destCellId`'s landblock coords are in-world; recenter streaming + prioritize-load the dest landblock (existing path); stash `_pendingArrival`; enter `Holding`. Re-send `LoginComplete` immediately (holtburger-conformant — `messages.rs:434`; do **not** wait for assets to send it). 2. Each frame in `Holding`, evaluate the **readiness predicate**: - `IsSpawnClaimUnhydratable(destCell)` → impossible claim: stop holding, place via the safety-net demote (loud log), exit PortalSpace. - `now > deadline` (timeout, ~10 s) → force-snap via safety-net demote + loud log, exit PortalSpace. (See §5 — failure-surfacing, not symptom-masking.) - `SampleTerrainZ(destPos) != null && (outdoor || IsSpawnCellReady(destCell))` → ready: go to 3. - else stay frozen, retry next frame. 3. `Placing`: call the **existing** `Resolve(destPos, destCell, Vector3.Zero, …)`. Because the cell is now hydrated, Resolve takes the #111 validated-claim branch → `WalkableFloorZNearest` grounds the player on the EnvCell floor. Snap entity + controller (existing `:4935-4939` code), exit PortalSpace, resume input. **Readiness predicate — reuse the #107 login triplet (no new query).** The hold gates on exactly the three checks the login auto-entry gate already uses (`GameWindow.cs:1010-1024`), evaluated against the teleport's `(destPos, destCell)` instead of the spawn claim: `SampleTerrainZ(destPos.X, destPos.Y) is not null` (destination terrain applied) ∧ (outdoor cell OR `IsSpawnCellReady(destCell)`); `IsSpawnClaimUnhydratable(destCell)` short-circuits an impossible claim to immediate placement. This reuses proven, validated code rather than introducing a parallel "landblock applied" query. **Dest-coord validation:** in `OnLivePositionUpdated`, reject a destination whose `(lbX, lbY)` is out of the world grid before recenter; log + abort the teleport hold rather than recenter to a phantom block. **Hydration decouple (§2.4):** move `BuildLoadedCell` + `CacheCellStruct` out of the `cellSubMeshes.Count > 0` guard in `BuildInteriorEntitiesForStreaming`. Gate each on its own non-null precondition. **Acceptance (G.3a):** the visual gate in §6. This gate also empirically settles #95 (does the flood blow up?) and the hydration coupling (does collision work?). ### 3.2 G.3b — #95 visibility bounding (REQUIRED — gate-confirmed 2026-06-13) **The G.3a gate confirmed the blowup** (9.1M instances/frame in `0x0007`), so this is the next blocker, not a conditional follow-up. The dungeon will not render until the portal-visibility flood is bounded to the dungeon's own cell adjacency. **Fix:** port retail `CEnvCell::grab_visible_cells` (`:311878`) — a cell with `seen_outside == 0` loads ZERO terrain and walks ONLY its `stab_list` of adjacent EnvCells; the portal graph is bounded by the dungeon's own cell adjacency, never a radius / never the whole resident cell set. This is a render-pipeline change in `PortalVisibilityBuilder` (the flap-/DO-NOT-RETRY-sensitive area) and needs its own grounding + brainstorm before implementation (verify the dat carries the stab_list and acdream's EnvCell loader parses it; confirm the `seen_outside` flag is read; decide how it composes with the outdoor-root look-in floods). **NOT a wing-it inline fix.** **Open question surfaced at the gate (possible Bug C):** even with Bug A fixed (placement keeps the dungeon prefix, `2ce5e5c`), the dungeon's negative-local-Y coordinate frame may cause the per-tick membership/landblock resolution to drift (the ACE `movement pre-validation failed` spam). Re-gate after Bug A to see if it persists; if so, fold the dungeon-coordinate membership handling into G.3b's grounding (it is plausibly the same `seen_outside` / cross-landblock root as #95). ### 3.3 G.3c — Portal-tunnel loading visual (faithful `TeleportAnimState`) **Goal:** the retail portal-space transition, ported faithfully (user decision 2026-06-13). Reconciles the older r09 §6 ("there is no loading screen") with the named-retail decomp where this FSM actually lives. **Oracle:** `gmSmartBoxUI::BeginTeleportAnimation` (`004d6300`, named-retail line 218888) + the per-frame FSM (`219405-219774`). States: `TAS_WORLD_FADE_OUT → TAS_TUNNEL_FADE_IN → TAS_TUNNEL / TAS_TUNNEL_CONTINUE → TAS_TUNNEL_FADE_OUT → TAS_WORLD_FADE_IN → (off)`. `m_pPortalSpace` is a `UIElement_Viewport` rendering the tunnel scene (creature-mode objects + `DISTANT_LIGHT` + smartbox FOV; `SetVisible(1)` on enter, `SetVisible(0)` on the `TAS_TUNNEL_FADE_OUT → TAS_WORLD_FADE_IN` edge at `219742-219747`). **Key architectural unification:** the `TAS_TUNNEL`/`TAS_TUNNEL_CONTINUE` **hold state's exit gates on the same readiness predicate as G.3a** — retail's loading visual and the hold-until-hydration gate are *one mechanism* (the tunnel is the visual form of the hold). G.3a ships the bare PortalSpace freeze; G.3c wraps it in the tunnel viewport + the fade FSM, exit-gated identically. **Port workflow:** grep-named → decompile `BeginTeleportAnimation` + the FSM → pseudocode (durations, fade math, viewport scene construction) → port → test. Detail deferred to the G.3c implementation phase; this spec fixes the design (states, transitions, the readiness-gated hold) + the oracle pointers. ### 3.4 G.3d — Recall game-actions Outbound **zero-payload** game-action builders (r09 §7.1): `TeleToLifestone 0x0063`, `TeleToHouse 0x0262`, `TeleToMansion 0x0278`, `TeleToMarketPlace 0x028D`, `RecallAllegianceHometown 0x02AB`, `TeleToPkArena 0x0027`. The client only sends the request; the server validates, plays the recall animation, then drives the **same** `PlayerTeleport → UpdatePosition` arrival flow. Value: (1) doubles as the **easy test lever** for G.3a/G.3c — `/ls` triggers a teleport with no portal-click choreography; (2) completes the recall UX (keybinds exist; the wire sends + return handling did not). Wire through the existing command bus. --- ## 4. Data flow (the teleport happy path) ``` 1. PlayerTeleport(0xF751) → OnTeleportStarted: enter PortalSpace, freeze input [G.3c: BeginTeleportAnimation(TAS_WORLD_FADE_OUT)] 2. fake UpdatePosition(destCell) → validate dest coords → recenter streaming to dest lb → prioritize-load dest lb → re-send LoginComplete 3. HOLD (TeleportArrivalController.Tick, each frame in PortalSpace): ready = SampleTerrainZ(destPos) != null && (outdoor || IsSpawnCellReady(destCell)) - not ready → stay frozen, retry [G.3c: tunnel holds in TAS_TUNNEL/_CONTINUE] - impossible → IsSpawnClaimUnhydratable → safety-net demote + loud log - timeout → force-snap + loud log + leave PortalSpace 4. READY → Resolve(destPos, destCell) → #111 validated-claim branch → WalkableFloorZNearest places on the EnvCell floor → SetPosition(entity + controller) → exit PortalSpace, resume input [G.3c: TAS_TUNNEL_FADE_OUT → TAS_WORLD_FADE_IN → off] ``` (ACE server send-order, for reference — `Player_Location.Teleport:686`: `PlayerTeleport(seq)` → fake `UpdatePosition` (start client load) → `DoTeleportPhysicsStateChanges` (hidden / ignoreCollisions) → real `UpdatePosition` → `OnTeleportComplete` after `CreateWorldObjectsCompleted`.) --- ## 5. Error handling | Failure | Handling | No-workaround rationale | |---|---|---| | Impossible / poisoned claim (cell id ∉ `[0x0100, 0x0100+NumCells)`, or no struct + no surface) | `IsSpawnClaimUnhydratable` → safety-net demote (`PhysicsEngine.Resolve` head, `:536-570`) + loud log; never hold forever | Reuses the validated #107/#111 reject; no new masking | | Dest LB fails to stream (worker crash / corrupt dat / OOB coords) | Timeout ceiling (~10 s) → force-snap + loud log + leave PortalSpace | **Surfaces** the failure (visible bad placement + log), does not freeze the client or silence the cause; gets a divergence-register row | | Mid-hold entity-rescue race | Already serialized by `_datLock` during recenter (verified, seam-3) | No change | The timeout is the one judgment call: holding forever on a never-hydrating landblock would soft-lock the client. The chosen behavior **fails loudly and visibly** (force-snap + log), which is the opposite of a symptom-masking grace period — it makes a broken teleport obvious rather than hiding it. It is recorded as a deliberate adaptation (retail loads synchronously; async streaming has no direct analog). --- ## 6. Testing & acceptance ### 6.1 Headless / unit - `TeleportArrivalController` FSM: `Idle → Holding → Placing` happy path; impossible-claim immediate reject; timeout force-snap; ready-predicate gating (fake `IsLandblockApplied` / `IsSpawnCellReady`). - Hydration-decouple test: a geometry-less EnvCell (empty render mesh, non-empty physics BSP) still gets `CacheCellStruct` + `BuildLoadedCell`. - `TeleportFlowTests`: fake `PlayerTeleport` + `UpdatePosition` wire → controller phase transitions + input-gate flips. - `DungeonLandblockDatProbeTests` (exists): pins `0x0125` = flat + 71 cells. - G.3c: `TeleportAnimState` FSM transition test (state sequence + the readiness-gated `TAS_TUNNEL` hold-exit). - G.3d: recall-builder byte tests (opcode + empty payload, per builder). ### 6.2 Visual gate (the acceptance test — after G.3a) Teleport into the meeting-hall dungeon via the portal: - Player stands **in the dungeon cell**, on the floor (not ocean, not falling). - The dungeon renders; navigate **3–5 rooms**; **walls block** movement. - **No ocean / no ACE `failed transition` spam.** - (Implicitly) the portal flood does **not** blow up (#95 check) and collision works in every room (hydration-coupling check). `ACDREAM_PROBE_CELL=1` + `ACDREAM_PROBE_VIEWER=1` + `ACDREAM_WB_DIAG=1` + the always-on `[snap]` / `live: teleport` lines capture the chain (the `launch-dungeon-diag.log` protocol from this session). ### 6.3 Per-installment build/test gates Each installment: `dotnet build` green + `dotnet test` green (App / Core / UI / Net suites) before it's "done"; G.3a additionally requires the visual gate. --- ## 7. Retail divergence register impact - **G.3a timeout force-snap** → NEW row (adaptation: async streaming hold has no synchronous-retail analog; retail loads the cell set synchronously before `SetPositionInternal`). - **Hydration decouple** → NO row (bug fix retiring an incidental render↔physics coupling; restores retail-correct independence). - **G.3c** → only a row if a faithful asset can't be reproduced (e.g. the tunnel viewport scene) and a documented courtesy substitute is shipped. - **#95 close-as-superseded** (if G.3b not triggered) → ISSUES.md note only. --- ## 8. Component boundaries (what each unit does / depends on) | Unit | Location | Does | Depends on | |---|---|---|---| | `TeleportArrivalController` | `AcDream.App/World/` | Owns the `Idle/Holding/Placing` phase + `_pendingArrival`; decides hold-vs-place each frame | readiness predicate (injected), `Resolve` (injected), PortalSpace state | | readiness predicate | `PhysicsEngine` (reused #107 triplet) | `SampleTerrainZ(pos)` ∧ (outdoor ∨ `IsSpawnCellReady(cell)`); `IsSpawnClaimUnhydratable(cell)` | `DataCache`, dat `LandBlockInfo` | | hydration decouple | `GameWindow.BuildInteriorEntitiesForStreaming` | `BuildLoadedCell` + `CacheCellStruct` gated on cellStruct/BSP, not render mesh | `cellStruct`, `PhysicsBSP` | | `TeleportAnimState` FSM (G.3c) | `AcDream.App` UI/render | Portal-tunnel fade FSM; hold-exit gated on the readiness predicate | `m_pPortalSpace` viewport, the readiness predicate | | recall builders (G.3d) | `AcDream.Core/Network/Actions` | Zero-payload outbound game actions | command bus | `AcDream.Core` gains no GL/window dependency. The controller + FSM live in `AcDream.App`; the readiness predicate's physics half lives in `AcDream.Core` (pure), its streaming half in `AcDream.App`. --- ## 9. References cited - **Current code (verified this session):** `GameWindow.cs` 4877-4961 (arrival), ~4971-4976 (`OnTeleportStarted`), 1010-1024 (#107 login gate), 11728-11748 (`IsSpawnClaimUnhydratable`), 5564-5651 (EnvCell hydration guard), 5941-6150 (`ApplyLoadedTerrainLocked`); `PhysicsEngine.cs` 468-472 (`IsSpawnCellReady`), 626-646 (#111 validated claim), 383-406 (`WalkableFloorZNearest`), 536-570 (Resolve safety net); `StreamingRegion.cs` 180-283 (`RecenterTo`); `StreamingController.cs` 120-149 (drain); `PortalVisibilityBuilder.cs` 131 (lbMask), 165 (enqueue-once); `CellTransit.cs` 515-516 (null-skip); `PhysicsDataCache.cs` 172 (null-BSP early-return). - **Decomp (named-retail):** `BeginTeleportAnimation` `004d6300` (line 218888) + the `TeleportAnimState` FSM 219405-219774; `m_pPortalSpace` viewport 218829/219363; `CEnvCell::grab_visible_cells` `:311878` (G.3b stab_list). - **holtburger:** `messages.rs:434` (client re-sends `LoginComplete` on teleport). - **ACE:** `Player_Location.Teleport:686` (send order); `Landblock.cs:575` (`IsDungeon`); `Player_Tick.cs:548-560` (single-landblock dungeons); recall handlers + `Portal.ActOnUse`/`AdjustDungeon`. - **r09 deepdive:** `docs/research/deepdives/r09-dungeon-portal-space.md` (EnvCell / CellPortal wire layout, recall taxonomy, the retail contract). - **Issues:** [#133](../../ISSUES.md), [#95](../../ISSUES.md). - **Digests (DO-NOT-RETRY tables apply):** `project_render_pipeline_digest`, `project_physics_collision_digest`. --- ## 10. Open questions (resolved here; revisit only if the gate disagrees) 1. **Loading visual now or later?** Faithful `TeleportAnimState` in G.3c (user decision). Unified with the G.3a hold (the tunnel IS the hold's visual). 2. **Hold timeout/failure?** Reject impossible claims instantly (`IsSpawnClaimUnhydratable`); hold plausible-but-slow with a ~10 s ceiling; on timeout force-snap + loud log (fail visibly, never freeze). 3. **Big-jump streaming?** Verified to work (Chebyshev recenter). Add only dest-coord validation; the readiness gate reuses `SampleTerrainZ` (no new streaming query). 4. **EnvCell placement vs flat terrain?** The #111 `WalkableFloorZNearest` EnvCell path (identical to the cellar path that already works); the flat terrain renders below. The gate guarantees the cell is hydrated before Resolve runs. 5. **(New, deferred to G.3b/implementation)** Does the dat carry a parsed `stab_list` for `grab_visible_cells` bounding? Only matters if the gate shows the #95 blowup.