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:
commit
5ac9d8c19c
53 changed files with 6691 additions and 439 deletions
455
docs/superpowers/specs/2026-06-13-dungeon-support-design.md
Normal file
455
docs/superpowers/specs/2026-06-13-dungeon-support-design.md
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue