From 90786c19e25664122021e5c8edc407261f8cea7d Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 13 Jun 2026 14:53:27 +0200 Subject: [PATCH] =?UTF-8?q?handoff:=20M1.5=20dungeon=20support=20(G.3)=20g?= =?UTF-8?q?rounded=20=E2=80=94=20design=20research=20+=20the=20terrain-les?= =?UTF-8?q?s-premise=20refutation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the 2026-06-13 dungeon-G.3 handoff doc + a dat-probe test that RESOLVED the pivotal design ambiguity. A research agent assumed dungeon landblocks are terrain-less (LandblockLoader.Load returns null -> "rewrite the pipeline for terrain-less landblocks", 13 seams). The dat probe refutes it: dungeon landblock 0x0125 has a flat (all-zero-height) LandBlock record PLUS 71 EnvCells and no buildings/objects -> it streams fine via the existing pipeline as a flat-terrain landblock. The real blocker (#133) is narrow: the teleport-arrival handler (GameWindow.cs:4928) snaps the player via physics.Resolve BEFORE the dungeon landblock streams in -> Resolve falls back to the resident Holtburg landblocks -> places the player at A9B3 ocean. Fix shape: hold-until-hydration (reuse the #107 IsSpawnCellReady gate for the teleport-arrival path) + place into the EnvCell + the retail TeleportAnimState portal-space FSM for the full-G.3 loading screen. ACE confirms dungeons are single-landblock, so "multi-landblock LOD" is moot. The handoff captures: this session's closes (#108-residual/#127/#125 gated, #116 partial), the M1.5 re-open decision, the corrected root cause, the 5-way reference grounding (holtburger/ACE/retail decomp + the dat probe), the design direction, and the open brainstorm questions. Next session: resume the brainstorm at "propose approaches" -> spec -> writing-plans -> implement. Suites green: App 264+1skip / Core 1445+2skip / UI 420 / Net 294. Co-Authored-By: Claude Fable 5 --- .../research/2026-06-13-dungeon-g3-handoff.md | 205 ++++++++++++++++++ .../DungeonLandblockDatProbeTests.cs | 76 +++++++ 2 files changed, 281 insertions(+) create mode 100644 docs/research/2026-06-13-dungeon-g3-handoff.md create mode 100644 tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs diff --git a/docs/research/2026-06-13-dungeon-g3-handoff.md b/docs/research/2026-06-13-dungeon-g3-handoff.md new file mode 100644 index 00000000..20f818dc --- /dev/null +++ b/docs/research/2026-06-13-dungeon-g3-handoff.md @@ -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. diff --git a/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs b/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs new file mode 100644 index 00000000..e1fe5a96 --- /dev/null +++ b/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs @@ -0,0 +1,76 @@ +using System.Linq; +using DatReaderWriter; +using DatReaderWriter.Options; +using DatLandBlock = DatReaderWriter.DBObjs.LandBlock; +using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo; +using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; +using Xunit; +using Xunit.Abstractions; + +namespace AcDream.Core.Tests.Conformance; + +/// +/// G.3 dungeon-support research probe (2026-06-13): resolve the pivotal +/// terrain-less-vs-ocean ambiguity for the meeting-hall dungeon landblock +/// 0x0125 (the teleport this session went to cell 0x01250126). Does a dungeon +/// landblock have a LandBlock (0xXXYYFFFF) terrain record at all, or only +/// LandBlockInfo + EnvCells? Output-only — no assertions. +/// +public sealed class DungeonLandblockDatProbeTests +{ + private readonly ITestOutputHelper _out; + public DungeonLandblockDatProbeTests(ITestOutputHelper output) => _out = output; + + [Fact] + public void Probe_Dungeon0125_vs_Holtburg_A9B4() + { + var datDir = ConformanceDats.ResolveDatDir(); + if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } + using var dats = new DatCollection(datDir, DatAccessType.Read); + + foreach (uint lb in new uint[] { 0x0125u, 0xA9B4u }) + { + _out.WriteLine($"=== landblock 0x{lb:X4} ==="); + + uint terrainId = (lb << 16) | 0xFFFFu; + var block = dats.Get(terrainId); + if (block is null) + { + _out.WriteLine($" LandBlock 0x{terrainId:X8}: NULL (no terrain record)"); + } + else + { + var heights = block.Height; + bool allZero = heights is not null && heights.All(h => h == 0); + int distinct = heights is null ? 0 : heights.Distinct().Count(); + _out.WriteLine($" LandBlock 0x{terrainId:X8}: present, Height[{heights?.Length ?? 0}] allZero={allZero} distinctIndices={distinct} first8=[{(heights is null ? "" : string.Join(",", heights.Take(8)))}]"); + } + + uint infoId = (lb << 16) | 0xFFFEu; + var info = dats.Get(infoId); + if (info is null) + { + _out.WriteLine($" LandBlockInfo 0x{infoId:X8}: NULL"); + } + else + { + _out.WriteLine($" LandBlockInfo 0x{infoId:X8}: NumCells={info.NumCells} Buildings={info.Buildings?.Count ?? 0} Objects={info.Objects?.Count ?? 0}"); + } + + // probe the first few EnvCells + int found = 0; + for (uint low = 0x0100u; low < 0x0110u; low++) + { + uint cellId = (lb << 16) | low; + var cell = dats.Get(cellId); + if (cell is not null) + { + found++; + if (found <= 3) + _out.WriteLine($" EnvCell 0x{cellId:X8}: present, CellStructure={cell.CellStructure} Portals={cell.CellPortals?.Count ?? 0} pos=({cell.Position.Origin.X:F1},{cell.Position.Origin.Y:F1},{cell.Position.Origin.Z:F1})"); + } + } + _out.WriteLine($" EnvCells 0x0100..0x010F present: {found}"); + } + } +}