test(p0): dat-backed conformance loader + characterized cottage-doorway topology

P0 (verbatim-spatial-pipeline-port) Tasks 1+2. ConformanceDats loads the
cottage-doorway cells from the real dats with their real ContainmentBsp;
CottageDoorwayCharacterizationTests maps the Holtburg 0140..017F indoor
neighborhood and pins the master-plan threshold building (origin
161.93,7.50,94.00): 0xA9B40170 vestibule (exit portal 0xFFFF + portal to
0171), 0xA9B40171 room. Grid math confirms the outdoor side is landcell
0xA9B40031 -> the 0031<->0170<->0171 ping-pong is verified real. Verified
interior points recorded for the point_in_cell/find_cell_list goldens.

Plan: docs/superpowers/plans/2026-06-03-p0-conformance-apparatus.md
Notes: docs/research/2026-06-03-p0-conformance-apparatus-notes.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-03 14:20:17 +02:00
parent a859116d5f
commit a90f34368f
4 changed files with 1000 additions and 0 deletions

View file

@ -0,0 +1,70 @@
# P0 — Conformance apparatus notes (characterized topology + goldens)
2026-06-03. Companion to [`docs/superpowers/plans/2026-06-03-p0-conformance-apparatus.md`](../superpowers/plans/2026-06-03-p0-conformance-apparatus.md).
Source of truth for the cell ids + golden inputs the P0 conformance tests pin. All values below
were **characterized from the real client dats** (not guessed) by
`CottageDoorwayCharacterizationTests` against `%USERPROFILE%\Documents\Asheron's Call`.
## Characterized Holtburg (landblock 0xA9B4) indoor neighborhood
Scanned `0xA9B40140..0xA9B4017F`. All cells load with a real ContainmentBsp and `seenOutside=1`.
Cells share a per-BUILDING world origin (the building's reference frame). The distinct buildings:
| Cell range | World origin | Stab | Identity |
|---|---|---|---|
| `0140..0150` (17 cells) | (130.50, 11.50, 94.00) | 17 | **Cottage-with-cellar** (#98 cellar saga; exit portals at 0145,014C,014E,014F,0150) |
| `0151..0169` (25 cells) | (107.50, 36.00, 94.00) | 24 | Larger building (inn) |
| `016A..016E` (5 cells) | (79.50, 37.50, 94.00) | 4 | Small building |
| **`016F..0175` (7 cells)** | **(161.93, 7.50, 94.00)** | 6 | **THE doorway-threshold building (master-plan 0170/0171)** |
| `0176..0178` | (65.21, 156.63, 66.00) | 2 | — |
| `0179..017A` | (158.18, 37.71, 94.00) | 1 | — |
| `017B..017F` | (161.72, 105.05, 66.00) | 4 | — |
## The pinned threshold topology (master-plan `0031↔0170↔0171`)
Building at world origin **(161.93, 7.50, 94.00)**:
| Cell | Role | Portal dests | seenOutside | BSP |
|---|---|---|---|---|
| `0xA9B40170` | **vestibule / doorway** | `[0xFFFF (exit→outdoor), 0x0171]` | 1 | real |
| `0xA9B40171` | **room** (behind the door) | `[0x0170, 0x0173, 0x0175]` | 1 | real |
Grid math confirms the outdoor side: origin (161.93, 7.50) → gridX = ⌊161.93/24⌋ = 6,
gridY = ⌊7.5/24⌋ = 0 → landcell id `6*8 + 0 + 1 = 49 = 0x31`**outdoor landcell `0xA9B40031`**.
So the player crossing the doorway traverses `0031 (outdoor) → 0170 (vestibule, via the 0xFFFF exit
portal) → 0171 (room)` — exactly the master-plan ping-pong `0031↔0170↔0171`. **Verified real.**
> Naming note: a 2026-05-21 capture dir called `0170/0171` the "inn 2nd floor"; that label was
> loose. By geometry it is the 7-cell building at (161.93, 7.50). Identity (cottage vs inn) is
> irrelevant to the conformance — what matters is `0170` carries the exit portal (the doorway) and
> `0171` is the room behind it.
## Golden interior points (verified `point_in_cell == true`)
Cell-LOCAL points whose `EnvCell.PointInCell(world)` returns true (the world form is
`Vector3.Transform(local, cellPhysics.WorldTransform)`):
| Cell | Interior LOCAL point | Interior WORLD point | Notes |
|---|---|---|---|
| `0xA9B40170` | (5.865, -8.449, 0.417) | (156.06, 15.95, 94.42) | 115/125 grid-inset points inside (small/irregular vestibule) |
| `0xA9B40171` | (6.55, -3.25, 4.60) | (157.01, 13.69, 95.53)* | bounds-center = bsphere origin; 125/125 inside (clean box) |
\* world recomputed by the test at runtime via the cell's WorldTransform; the value above is the
firstInside probe — the canonical bsphere-origin local (6.55,-3.25,4.60) is also fully interior.
These are **retail-faithful by construction**: the ContainmentBsp is loaded from the same dats
retail loads, so a geometrically-correct containment answer is the retail answer.
## Golden provenance summary
| Golden | Provenance | Status |
|---|---|---|
| `point_in_cell` (interior true, far-away false) | geometric (real dat BSP) | autonomous (Task 3) |
| `find_cell_list` deep-inside picks `0171`/`0170` | geometric (real dat cells) | autonomous (Task 4) |
| `find_cell_list` doorway-threshold pick | **retail cdb trace** | Task 6 — USER GATE / mine existing traces |
| PVS visible-set | retail cdb `cell_draw_list` trace | deferred to P4 (Task 7 scaffold) |
## P1-entry checklist (filled at end of P0)
- [ ] Threshold golden GREEN (acdream already matches retail) or RED (P1 must fix) — recorded here.
- [ ] "P0 gate met: ≥1 retail-trace-backed assertion exists" — stated explicitly so P1 can begin.

View file

@ -0,0 +1,709 @@
# P0 — Conformance Apparatus Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (inline) to
> implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build the headless, dat-backed conformance apparatus that lets every later phase
(P1 membership, P2 door collision, P3 camera, P4 PView render) prove a port is **verbatim
retail** — not "vibes" — by asserting real-retail outcomes (`point_in_cell`, `find_cell_list`,
PVS visible-set) against fixtures loaded from the real client dats and against captured retail
cdb traces.
**Architecture:** Three layers. (1) A dat-backed **fixture loader** that hydrates the Holtburg
cottage-doorway cell neighborhood from the real dats with their *real* containment BSPs (no
synthetic BSP, no JSON round-trip). (2) **Golden conformance tests** that pin retail-faithful
outcomes — `point_in_cell` is geometric ground truth (the BSP *is* retail data, loaded from the
same dats retail loads); `find_cell_list` pins the membership pick, with the subtle
doorway-threshold case backed by a captured **retail cdb trace**. (3) **cdb value-capture
tooling** — a new script that dumps `find_cell_list`'s cell-id argument/return at the threshold
(value, not just hit-count), plus a parser that turns its log into a golden fixture. The live
capture run is the single user-gated step; everything else is autonomous + headless.
**Tech Stack:** C# / .NET 10, xUnit, `DatReaderWriter` (`DatCollection.Get<T>`), the existing
`PhysicsDataCache` / `CellTransit` / `BSPQuery` engine, cdb (`acclient.pdb`) per the CLAUDE.md
retail-debugger toolchain.
**Scope note — what P0 is NOT.** P0 does not change any production membership/collision/render
code. It only adds `tests/` + `tools/cdb/` + `docs/`. The PVS visible-set golden is
*scaffolded* (structure + retail anchor + a skipped placeholder) but not filled — a retail
`cell_draw_list` trace is a P3/P4-coupled capture and the PVS code itself is replaced in P4.
P0's load-bearing deliverable is the **membership** goldens (`point_in_cell` + `find_cell_list`)
that P1 consumes immediately, plus ≥1 assertion backed by a real retail trace (the P1 gate).
---
## File Structure
| File | Responsibility |
|---|---|
| `tests/AcDream.Core.Tests/Conformance/ConformanceDats.cs` (create) | Shared dat-dir resolution + one-call headless load of an `EnvCell` (real `ContainmentBsp`) and a cached `CellPhysics` by cell id. The single seam tests use to reach the real dats. |
| `tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs` (create) | Characterize-and-pin the real cottage-doorway cell topology (ids, worldOrigin, portals incl. the `0xFFFF` exit portal, `seenOutside`, has-ContainmentBsp). Discovers the exact IDs the rest of P0 uses. |
| `tests/AcDream.Core.Tests/Conformance/PointInCellConformanceTests.cs` (create) | Golden `point_in_cell`: assert containment for geometrically-known points against the real BSP. Retail-faithful by construction. |
| `tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs` (create) | Golden `find_cell_list`: unambiguous picks (clearly-inside / clearly-outside) from dats + the doorway-threshold pick pinned to a captured retail trace. |
| `tests/AcDream.Core.Tests/Conformance/RetailTrace.cs` (create) | Parser: read a `find-cell-list` retail cdb log → strongly-typed `RetailCellPick[]` golden records. TDD'd against a checked-in sample log line. |
| `tools/cdb/find-cell-list-capture.cdb` (create) | New cdb script: breakpoint `CObjCell::find_cell_list`, dump the input position + the returned containing cell id (value capture). Ready for the user to run against live retail at the cottage doorway. |
| `tools/cdb/README-find-cell-list-capture.md` (create) | Operator runbook for the capture: prerequisites, exact launch command, what to walk, where the log lands, how to fold it into the golden. |
| `docs/research/2026-06-03-p0-conformance-apparatus-notes.md` (create) | Living notes: the characterized cottage-doorway topology, the golden values + their provenance (geometric vs retail-trace), and the P1-entry checklist. |
---
## Task 1 — Shared dat-backed fixture loader
**Files:**
- Create: `tests/AcDream.Core.Tests/Conformance/ConformanceDats.cs`
The loader mirrors the proven dat-read pattern in
`tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs:184-219`
(`DatCollection.Get<EnvCell>``Get<Environment>``CellStruct` → physics-verbatim
`worldTransform`) and the `EnvCell.FromDat` derivation
(`src/AcDream.Core/World/Cells/EnvCell.cs:42-76`). It returns BOTH a `EnvCell` (for
`PointInCell`) and a cached `CellPhysics` (for `CellTransit`), so membership tests have one seam.
- [ ] **Step 1: Write the loader**
```csharp
using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.World.Cells;
using DatReaderWriter;
using DatReaderWriter.Options;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
using Env = System.Environment;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// P0 conformance apparatus — headless load of the real Holtburg dats.
/// Tests that need real cell geometry (the retail containment BSP) resolve
/// the dat dir here and load cells via <see cref="LoadEnvCell"/>. Returns
/// null dat dir when the dats are absent (CI) so callers can skip cleanly,
/// matching DoorBugTrajectoryReplayTests.ResolveDatDir.
/// </summary>
public static class ConformanceDats
{
private const uint EnvironmentFilePrefix = 0x0D000000u; // dat namespace for Environment files
/// <summary>The Holtburg landblock these fixtures live in.</summary>
public const uint HoltburgLandblock = 0xA9B40000u;
/// <summary>Resolve the client dat directory, or null if unavailable (skip the test).</summary>
public static string? ResolveDatDir()
{
var fromEnv = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Env.GetFolderPath(Env.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
/// <summary>The physics-verbatim cell→world transform (no +2cm render lift).</summary>
public static Matrix4x4 WorldTransform(DatEnvCell datCell) =>
Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) *
Matrix4x4.CreateTranslation(datCell.Position.Origin);
/// <summary>
/// Load one EnvCell from the dats with its REAL containment BSP, and register
/// it into <paramref name="cache"/> as a CellPhysics. Returns the high-level
/// EnvCell (PointInCell) so a single load serves both membership predicates.
/// </summary>
public static EnvCell LoadEnvCell(DatCollection dats, PhysicsDataCache cache, uint cellId)
{
var datCell = dats.Get<DatEnvCell>(cellId)
?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found in dats");
var environment = dats.Get<DatEnvironment>(EnvironmentFilePrefix | datCell.EnvironmentId)
?? throw new InvalidOperationException($"Environment 0x{datCell.EnvironmentId:X8} not found");
if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null)
throw new InvalidOperationException($"CellStruct {datCell.CellStructure} missing from environment");
var world = WorldTransform(datCell);
cache.CacheCellStruct(cellId, datCell, cellStruct, world); // physics CellPhysics (real CellBSP)
return EnvCell.FromDat(cellId, datCell, cellStruct, world); // render/containment EnvCell (real ContainmentBsp)
}
}
```
- [ ] **Step 2: Verify it compiles**
Run: `dotnet build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug`
Expected: build succeeds (no test yet — Task 2 exercises it).
- [ ] **Step 3: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/ConformanceDats.cs
git commit -m "test(p0): dat-backed cottage-doorway fixture loader (ConformanceDats)"
```
---
## Task 2 — Characterize and pin the cottage-doorway topology
**Files:**
- Create: `tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs`
- Create: `docs/research/2026-06-03-p0-conformance-apparatus-notes.md`
The master plan names the neighborhood loosely (`0xA9B4003x` + `0xA9B4017x`), and the `0170`/`0171`
ids are reused across buildings in landblock `0xA9B4`. So this task **discovers** the real
topology and pins it. The candidate indoor ids are `0xA9B40170` and `0xA9B40171` (the
ping-pong pair named in the master plan §0). The test prints each cell's structure on first run;
fill the asserts from the printout, then they become the pinned characterization.
- [ ] **Step 1: Write the characterization test (observe form)**
```csharp
using System;
using System.Linq;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
public class CottageDoorwayCharacterizationTests
{
private readonly ITestOutputHelper _out;
public CottageDoorwayCharacterizationTests(ITestOutputHelper output) => _out = output;
// Candidate indoor cells of the cottage-doorway ping-pong (master plan §0: 0031↔0170↔0171).
public static readonly uint[] CandidateIndoor = { 0xA9B40170u, 0xA9B40171u };
[Fact]
public void Characterize_CottageDoorwayCells_PrintStructure()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
foreach (var id in CandidateIndoor)
{
var cell = ConformanceDats.LoadEnvCell(dats, cache, id);
var phys = cache.GetCellStruct(id)!;
var origin = System.Numerics.Vector3.Transform(
System.Numerics.Vector3.Zero, phys.WorldTransform);
bool hasExitPortal = phys.Portals!.Any(p => p.OtherCellId == 0xFFFFu);
_out.WriteLine(
$"0x{id:X8}: worldOrigin=({origin.X:F2},{origin.Y:F2},{origin.Z:F2}) " +
$"seenOutside={cell.SeenOutside} hasContainmentBsp={cell.ContainmentBsp?.Root is not null} " +
$"portals={phys.Portals!.Count} exitPortal={hasExitPortal} " +
$"stab={cell.StabList.Count} " +
$"portalDests=[{string.Join(",", phys.Portals!.Select(p => $"0x{p.OtherCellId:X4}"))}]");
}
}
}
```
- [ ] **Step 2: Run it and READ the printed structure**
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~Characterize_CottageDoorwayCells" -l "console;verbosity=detailed"`
Expected: PASS, with two `0xA9B401XX: worldOrigin=... portalDests=[...]` lines in the output.
**Record those lines** in `docs/research/2026-06-03-p0-conformance-apparatus-notes.md` under a
"Characterized topology" heading. They are the source of truth for Tasks 2b/3.
> If a candidate id throws "not found in dats", it is the wrong id — widen the candidate scan to
> `0xA9B40170..0xA9B40179` in the loop, re-run, and identify the two cells whose `worldOrigin`
> matches the cottage (low Y, near the `014x` cellar) and that form a portal pair (one's
> `portalDests` contains the other). Pin those two ids.
- [ ] **Step 3: Pin the discovered values (assert form)**
Replace the print-only body with explicit asserts using the recorded values, e.g. (substitute
the REAL numbers you recorded — these are illustrative):
```csharp
[Fact]
public void CottageDoorway_Cell0170_IsVestibuleWithExitPortal()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
var cell = ConformanceDats.LoadEnvCell(dats, cache, 0xA9B40170u);
var phys = cache.GetCellStruct(0xA9B40170u)!;
Assert.True(cell.ContainmentBsp?.Root is not null, "real cell BSP must load");
Assert.NotEmpty(phys.Portals!);
// Pin whatever the characterization printed — exit portal presence is the load-bearing fact:
Assert.Contains(phys.Portals!, p => p.OtherCellId == 0xFFFFu || p.OtherCellId == 0x0171u);
}
```
Keep the `Characterize_..._PrintStructure` test (it documents the data and re-runs cheaply).
- [ ] **Step 4: Run to verify the pinned asserts pass**
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~CottageDoorwayCharacterizationTests"`
Expected: PASS (both tests).
- [ ] **Step 5: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs docs/research/2026-06-03-p0-conformance-apparatus-notes.md
git commit -m "test(p0): characterize + pin cottage-doorway cell topology from dats"
```
---
## Task 3 — `point_in_cell` golden conformance
**Files:**
- Create: `tests/AcDream.Core.Tests/Conformance/PointInCellConformanceTests.cs`
- Modify: `docs/research/2026-06-03-p0-conformance-apparatus-notes.md`
Retail's `point_in_cell` for an EnvCell *is* `BSPTREE::point_in_cell_bsp` against `cell_bsp`
(master plan A6; reference doc §6.3). Our `EnvCell.PointInCell``BSPQuery.PointInsideCellBsp`
is the port. The BSP is loaded from the *same dats retail loads*, so a geometrically-correct
containment answer **is** the retail answer. Goldens: a point at the cell's world origin-ish
interior must be inside; a point far outside the landblock must be outside; the matching point
must be inside exactly one of the doorway pair.
- [ ] **Step 1: Write the failing test**
```csharp
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
namespace AcDream.Core.Tests.Conformance;
public class PointInCellConformanceTests
{
[Fact]
public void PointInCell_InteriorPoint_IsInsideItsOwnCell()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
var cell = ConformanceDats.LoadEnvCell(dats, cache, 0xA9B40171u);
var phys = cache.GetCellStruct(0xA9B40171u)!;
// A point just above the cell's covering-sphere centre, in world space, is interior.
var localCentre = phys.CellBSP is not null
? cache.GetCellStruct(0xA9B40171u)!.WorldTransform // placeholder; replaced below
: default;
// Use the cell's bounding-sphere origin (cell-local) lifted to world:
var worldCentre = Vector3.Transform(CharacterizedInteriorLocal_0171, phys.WorldTransform);
Assert.True(cell.PointInCell(worldCentre),
$"interior point {worldCentre} must be inside cell 0x0171's BSP");
}
[Fact]
public void PointInCell_FarAwayPoint_IsOutside()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
var cell = ConformanceDats.LoadEnvCell(dats, cache, 0xA9B40171u);
var faraway = new Vector3(10000f, 10000f, 10000f);
Assert.False(cell.PointInCell(faraway), "a point 10 km away cannot be inside");
}
// Pin this from Task 2's characterization: a cell-LOCAL point clearly inside 0x0171's volume
// (e.g. its bounding-sphere origin, or (0,0,1) if the cell is centred on its local origin).
private static readonly Vector3 CharacterizedInteriorLocal_0171 = new(0f, 0f, 1.0f);
}
```
- [ ] **Step 2: Run to verify it fails (or reveals the right interior point)**
Run: `dotnet test ... --filter "FullyQualifiedName~PointInCellConformanceTests" -l "console;verbosity=detailed"`
Expected: `PointInCell_FarAwayPoint_IsOutside` PASSES immediately. `..._IsInsideItsOwnCell` may
FAIL if `CharacterizedInteriorLocal_0171` isn't actually inside — that's the signal to use the
real interior point from Task 2 (the cell's bounding-sphere origin, printed by adding it to the
characterization output).
- [ ] **Step 3: Fix the interior point from characterization data**
Augment the Task-2 print to also emit the cell's bounding-sphere origin (cell-local), then set
`CharacterizedInteriorLocal_0171` to a point you've confirmed is inside (the sphere origin, or a
point nudged toward the floor). Remove the dead `localCentre` placeholder line.
- [ ] **Step 4: Run to verify both pass**
Run: `dotnet test ... --filter "FullyQualifiedName~PointInCellConformanceTests"`
Expected: PASS (both).
- [ ] **Step 5: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/PointInCellConformanceTests.cs docs/research/2026-06-03-p0-conformance-apparatus-notes.md
git commit -m "test(p0): point_in_cell golden conformance vs real dat BSP"
```
---
## Task 4 — `find_cell_list` golden conformance (unambiguous cases)
**Files:**
- Create: `tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs`
`CellTransit.FindCellList(cache, sphereCentre, radius, currentCellId)` is the membership pick.
Unambiguous goldens come straight from the real dats: a sphere clearly inside room `0x0171`
returns `0x0171`; a sphere clearly inside vestibule `0x0170` returns `0x0170`. These pin the pick
without needing a retail trace (the geometry is retail data). The *threshold* case (ping-pong)
is Task 6, backed by a retail trace.
- [ ] **Step 1: Write the failing test**
```csharp
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
namespace AcDream.Core.Tests.Conformance;
public class FindCellListConformanceTests
{
private const float FootRadius = 0.4f; // retail player foot-sphere radius (PhysicsBody default)
[Fact]
public void FindCellList_DeepInsideRoom0171_Returns0171()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
ConformanceDats.LoadEnvCell(dats, cache, 0xA9B40170u);
var room = ConformanceDats.LoadEnvCell(dats, cache, 0xA9B40171u);
var phys = cache.GetCellStruct(0xA9B40171u)!;
// A world point confirmed interior to 0x0171 in Task 3:
var inside0171 = Vector3.Transform(DeepInsideLocal_0171, phys.WorldTransform);
Assert.True(room.PointInCell(inside0171), "fixture sanity: point must be inside 0x0171");
// find_cell_list seeded from the room itself must return the room.
uint picked = CellTransit.FindCellList(cache, inside0171, FootRadius, 0xA9B40171u);
Assert.Equal(0xA9B40171u, picked);
}
// Pin from Task 3 (a cell-local point well inside 0x0171, away from any portal plane):
private static readonly Vector3 DeepInsideLocal_0171 = new(0f, 0f, 1.0f);
}
```
- [ ] **Step 2: Run to verify**
Run: `dotnet test ... --filter "FullyQualifiedName~FindCellListConformanceTests" -l "console;verbosity=detailed"`
Expected: PASS. If it returns a different cell, record the actual return in the notes doc and
investigate whether the seed point is too near a portal plane (move it deeper). A genuine
divergence here is itself a finding — document it; do NOT change production code in P0.
- [ ] **Step 3: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs
git commit -m "test(p0): find_cell_list golden — unambiguous interior picks from dats"
```
---
## Task 5 — Retail-trace parser + cdb value-capture tooling
**Files:**
- Create: `tests/AcDream.Core.Tests/Conformance/RetailTrace.cs`
- Create: `tools/cdb/find-cell-list-capture.cdb`
- Create: `tools/cdb/README-find-cell-list-capture.md`
The autonomous half of the retail-trace golden: the **parser** (TDD'd against a sample line) and
the **capture script** (ready to run). The capture *run* is the user gate in Task 6.
The cdb script breakpoints `CObjCell::find_cell_list` (`acclient!CObjCell::find_cell_list`,
`0x52b4e0`). `this` (the seed cell, `thiscall``ecx`) carries `objcell_id` at a known offset;
the position is an argument. We log the seed cell id and the position, and (via a return
breakpoint) the picked cell id. The exact field offsets are confirmed at capture time with
`dt acclient!CObjCell @ecx` (CLAUDE.md retail-debugger watchouts). The script emits lines of the
form `[fcl] seed=0xHHHHHHHH px=<f> py=<f> pz=<f> picked=0xHHHHHHHH`.
- [ ] **Step 1: Write the failing parser test**
```csharp
using System.Numerics;
using Xunit;
namespace AcDream.Core.Tests.Conformance;
public class RetailTraceTests
{
[Fact]
public void Parse_FindCellListLine_YieldsSeedPosAndPicked()
{
const string line =
"[fcl] seed=0xA9B40170 px=141.5000 py=7.2200 pz=92.7400 picked=0xA9B40171";
var rec = RetailTrace.ParseFindCellList(line);
Assert.NotNull(rec);
Assert.Equal(0xA9B40170u, rec!.SeedCellId);
Assert.Equal(new Vector3(141.5f, 7.22f, 92.74f), rec.Position);
Assert.Equal(0xA9B40171u, rec.PickedCellId);
}
[Fact]
public void Parse_NonMatchingLine_ReturnsNull()
{
Assert.Null(RetailTrace.ParseFindCellList("[BP4] find_collisions hit#10170 collide=0"));
}
}
```
- [ ] **Step 2: Run to verify it fails**
Run: `dotnet test ... --filter "FullyQualifiedName~RetailTraceTests"`
Expected: FAIL — `RetailTrace` does not exist.
- [ ] **Step 3: Write the parser**
```csharp
using System;
using System.Globalization;
using System.Numerics;
using System.Text.RegularExpressions;
namespace AcDream.Core.Tests.Conformance;
/// <summary>A single retail find_cell_list pick captured via cdb (golden oracle).</summary>
public sealed record RetailCellPick(uint SeedCellId, Vector3 Position, uint PickedCellId);
/// <summary>Parser for the find-cell-list-capture.cdb log format.</summary>
public static class RetailTrace
{
private static readonly Regex Fcl = new(
@"^\[fcl\]\s+seed=0x(?<seed>[0-9A-Fa-f]{1,8})\s+" +
@"px=(?<px>-?\d+(\.\d+)?)\s+py=(?<py>-?\d+(\.\d+)?)\s+pz=(?<pz>-?\d+(\.\d+)?)\s+" +
@"picked=0x(?<picked>[0-9A-Fa-f]{1,8})\s*$",
RegexOptions.Compiled);
public static RetailCellPick? ParseFindCellList(string line)
{
var m = Fcl.Match(line);
if (!m.Success) return null;
var ci = CultureInfo.InvariantCulture;
return new RetailCellPick(
SeedCellId: Convert.ToUInt32(m.Groups["seed"].Value, 16),
Position: new Vector3(
float.Parse(m.Groups["px"].Value, ci),
float.Parse(m.Groups["py"].Value, ci),
float.Parse(m.Groups["pz"].Value, ci)),
PickedCellId: Convert.ToUInt32(m.Groups["picked"].Value, 16));
}
}
```
- [ ] **Step 4: Run to verify it passes**
Run: `dotnet test ... --filter "FullyQualifiedName~RetailTraceTests"`
Expected: PASS (both).
- [ ] **Step 5: Write the cdb capture script**
```
$$ find-cell-list-capture.cdb — value-capture of CObjCell::find_cell_list at the cottage doorway.
$$ Logs the seed cell id + input position + picked cell id (NOT just a hit count).
$$ Prereqs: retail in-world at the Holtburg cottage doorway; PDB matches (check_exe_pdb.py = MATCH).
$$ Offsets for objcell_id + the position arg are CONFIRMED at runtime with `dt acclient!CObjCell @ecx`
$$ and `dv` — edit the @ecx+OFFSET below if the dt dump shows a different layout.
.logopen C:\Users\erikn\source\repos\acdream\find-cell-list-capture.log
.sympath C:\Users\erikn\source\repos\acdream\refs
.symopt+ 0x40
.reload /f acclient.exe
r $t0 = 0
$$ Entry: dump seed cell id (this->objcell_id @ ecx+0x20 — VERIFY with dt) + the position arg.
bp acclient!CObjCell::find_cell_list "r $t0 = @$t0 + 1; .printf /D \"[fcl-entry#%d] seed=0x%08x\\n\", @$t0, poi(@ecx+0x20); .if (@$t0 >= 4000) { qd } .else { gc }"
.printf \"find_cell_list capture armed. Walk SLOWLY in/out of the cottage doorway now.\\n\"
g
```
- [ ] **Step 6: Write the operator runbook**
Create `tools/cdb/README-find-cell-list-capture.md` with: the `check_exe_pdb.py` MATCH
precondition; the exact PowerShell launch (`cdb.exe -pn acclient.exe -cf find-cell-list-capture.cdb`);
the instruction to first run `dt acclient!CObjCell @ecx` once to confirm the `objcell_id` offset
and `dv` to find the position arg, then edit the script; the walk to perform (stand in the
doorway, step in and out across the threshold 510×); where the log lands; and how to hand the
log to Task 6.
- [ ] **Step 7: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/RetailTrace.cs tools/cdb/find-cell-list-capture.cdb tools/cdb/README-find-cell-list-capture.md
git commit -m "test(p0): retail find_cell_list trace parser + value-capture cdb script"
```
---
## Task 6 — The retail-trace golden (USER GATE) + P1-entry checklist
**Files:**
- Modify: `tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs`
- Create: `tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log` (from the capture)
- Modify: `docs/research/2026-06-03-p0-conformance-apparatus-notes.md`
This task has the one irreducible user-gated step: a live retail cdb capture. Everything is
prepped (Task 5). After the capture, encode ≥1 retail pick as a golden assertion — satisfying the
kickoff's "do NOT start P1 before ≥1 golden retail-trace assertion exists."
- [ ] **Step 1: USER GATE — capture the retail trace**
Mining the existing committed traces first (`docs/research/2026-05-21-a6-captures/scen1_inn_doorway/retail.decoded.log`
and `scen4_cottage_cellar/retail.decoded.log`): grep for any line carrying a `find_cell_list`
cell-id pick. If one exists in usable form, use it (no new capture needed). Otherwise, ask the
user to run `tools/cdb/find-cell-list-capture.cdb` per its README against live retail at the
cottage doorway, and place the resulting log at
`tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log`.
- [ ] **Step 2: Add the trace-backed golden**
For each captured `RetailCellPick`, assert acdream's `CellTransit.FindCellList` returns the same
picked cell for the same `(SeedCellId, Position)`:
```csharp
[Fact]
public void FindCellList_DoorwayThreshold_MatchesRetailTrace()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
var fixturePath = System.IO.Path.Combine(
System.AppContext.BaseDirectory, "Conformance", "Fixtures", "find-cell-list-threshold.log");
if (!System.IO.File.Exists(fixturePath)) return; // gate not yet satisfied — skip until captured
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
foreach (var id in new[] { 0xA9B40170u, 0xA9B40171u })
ConformanceDats.LoadEnvCell(dats, cache, id);
foreach (var raw in System.IO.File.ReadAllLines(fixturePath))
{
var pick = RetailTrace.ParseFindCellList(raw);
if (pick is null) continue;
uint ours = CellTransit.FindCellList(cache, pick.Position, 0.4f, pick.SeedCellId);
Assert.Equal(pick.PickedCellId, ours);
}
}
```
> Per master-plan no-shortcuts rule §4: if this assertion fails, it means *acdream diverges from
> retail* at the threshold — that is the P1 work, captured as a RED conformance test. Leave it RED
> (documents-the-bug) and let P1 turn it GREEN. Do NOT weaken the assertion.
- [ ] **Step 3: Wire the fixture into the test project so it copies to output**
Modify `tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj` to copy `Conformance/Fixtures/**`
to the output dir (mirror how existing `Fixtures/issue98/**` is copied — find that `<ItemGroup>`
and add the `Conformance/Fixtures` glob, or confirm a wildcard already covers it).
- [ ] **Step 4: Run the full conformance suite**
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~Conformance"`
Expected: all GREEN, except possibly the documents-the-bug threshold test if acdream diverges
(that RED is a finding for P1, not a P0 failure).
- [ ] **Step 5: Write the P1-entry checklist into the notes doc**
In `docs/research/2026-06-03-p0-conformance-apparatus-notes.md`, record: the characterized
topology; every golden + its provenance (geometric vs retail-trace); whether the threshold golden
is GREEN (acdream already matches) or RED (P1 must fix); and the explicit statement "P0 gate met:
≥1 retail-trace-backed assertion exists" so P1 can begin.
- [ ] **Step 6: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/ docs/research/2026-06-03-p0-conformance-apparatus-notes.md tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj
git commit -m "test(p0): retail-trace-backed find_cell_list threshold golden (P1 gate met)"
```
---
## Task 7 — PVS-golden scaffold (structure only, fill deferred to P3/P4)
**Files:**
- Create: `tests/AcDream.Core.Tests/Conformance/PvsConformanceTests.cs`
The master plan lists "PVS visible-set for a given (cell, eye) matches" under P0, but a retail
`cell_draw_list` trace is a P3/P4-coupled capture and the PVS code is replaced in P4. So P0
scaffolds the test (intent + retail anchor + a skipped placeholder) so the structure exists when
P4 captures the retail visible-set.
- [ ] **Step 1: Write the scaffold (skipped)**
```csharp
using Xunit;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// PVS (portal-visibility) conformance — scaffold. Retail oracle:
/// PView::ConstructView @ 0x005a57b0 (pc:433750) produces cell_draw_list; the golden is a
/// captured retail cell_draw_list for a given (viewer_cell, eye). Filled in P3/P4 when the
/// camera viewer-cell + ConstructView ports land and a retail cell_draw_list trace is captured
/// (new cdb script breakpointing PView::DrawCells / cell_draw_list, sibling to
/// find-cell-list-capture.cdb). See docs/research/2026-06-02-retail-render-pipeline-full-reference.md §3.
/// </summary>
public class PvsConformanceTests
{
[Fact(Skip = "P0 scaffold — filled in P4 with a captured retail cell_draw_list trace")]
public void Pvs_CottageInterior_MatchesRetailCellDrawList() { }
}
```
- [ ] **Step 2: Verify it builds + reports as skipped**
Run: `dotnet test ... --filter "FullyQualifiedName~PvsConformanceTests"`
Expected: 1 skipped, 0 failed.
- [ ] **Step 3: Commit**
```bash
git add tests/AcDream.Core.Tests/Conformance/PvsConformanceTests.cs
git commit -m "test(p0): PVS-golden conformance scaffold (filled in P4)"
```
---
## Self-Review
**Spec coverage (master-plan P0 + reference §G4):**
- "Headless fixtures of the cottage neighborhood loaded from real dats" → Task 1 (`ConformanceDats.LoadEnvCell`) + Task 2 (characterize).
- "`point_in_cell` matches" → Task 3 (geometric goldens vs real BSP).
- "`find_cell_list` returns the same cell as a captured retail trace at the threshold" → Task 4 (unambiguous) + Task 6 (threshold, retail-trace-backed).
- "the PVS visible-set ... matches" → Task 7 (scaffold; fill in P4, documented).
- "Use the existing `ACDREAM_CAPTURE_RESOLVE` + cdb retail traces" → Task 5 (parser + capture script) + Task 6 (mine existing traces first, else live capture).
- "≥1 golden retail-trace assertion before P1" → Task 6 (the P1 gate).
**Placeholder scan:** Interior-point constants (`*_Local_0171`) and pinned cell asserts are
explicitly characterization-derived (Task 2/3 print-then-pin) — concrete process, not hand-waving.
The cdb field offset (`ecx+0x20`) is flagged as runtime-verified (`dt acclient!CObjCell @ecx`).
**Type consistency:** `ConformanceDats.LoadEnvCell` returns `EnvCell`; `cache.GetCellStruct(id)`
returns `CellPhysics` (matches `DoorBugTrajectoryReplayTests`); `CellTransit.FindCellList(cache,
Vector3, float, uint) → uint` (matches the membership map); `RetailTrace.ParseFindCellList(string)
→ RetailCellPick?`. Consistent across Tasks 1/3/4/6.
**Risks / open verifications (resolve during execution, not blocking):**
- `PhysicsDataCache.CacheCellStruct` + `GetCellStruct` + `CellPhysics.WorldTransform/Portals/CellBSP`
member names are from the door-test read at `DoorBugTrajectoryReplayTests.cs:213-225`; verify
exact casing on first compile.
- The two cottage-doorway ids may not be `0170`/`0171` — Task 2 discovers the truth and the
later tasks consume whatever Task 2 pins.
- Whether the existing committed retail traces already contain a usable `find_cell_list` pick is
checked first in Task 6 Step 1 before asking the user for a live capture.

View file

@ -0,0 +1,66 @@
using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.World.Cells;
using DatReaderWriter;
using DatReaderWriter.Options;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
using Env = System.Environment;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// P0 conformance apparatus — headless load of the real Holtburg dats.
/// Tests that need real cell geometry (the retail containment BSP) resolve
/// the dat dir here and load cells via <see cref="LoadEnvCell"/>. Returns
/// null dat dir when the dats are absent (CI) so callers can skip cleanly,
/// matching DoorBugTrajectoryReplayTests.ResolveDatDir.
///
/// Mirrors the proven dat-read pattern at
/// DoorBugTrajectoryReplayTests.cs:184-219 + EnvCell.FromDat (EnvCell.cs:42-76).
/// </summary>
public static class ConformanceDats
{
private const uint EnvironmentFilePrefix = 0x0D000000u; // dat namespace for Environment files
/// <summary>The Holtburg landblock these fixtures live in.</summary>
public const uint HoltburgLandblock = 0xA9B40000u;
/// <summary>Resolve the client dat directory, or null if unavailable (skip the test).</summary>
public static string? ResolveDatDir()
{
var fromEnv = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Env.GetFolderPath(Env.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
/// <summary>The physics-verbatim cell→world transform (no +2cm render lift).</summary>
public static Matrix4x4 WorldTransform(DatEnvCell datCell) =>
Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) *
Matrix4x4.CreateTranslation(datCell.Position.Origin);
/// <summary>
/// Load one EnvCell from the dats with its REAL containment BSP, and register
/// it into <paramref name="cache"/> as a CellPhysics. Returns the high-level
/// EnvCell (PointInCell) so a single load serves both membership predicates.
/// </summary>
public static EnvCell LoadEnvCell(DatCollection dats, PhysicsDataCache cache, uint cellId)
{
var datCell = dats.Get<DatEnvCell>(cellId)
?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found in dats");
var environment = dats.Get<DatEnvironment>(EnvironmentFilePrefix | datCell.EnvironmentId)
?? throw new InvalidOperationException($"Environment 0x{datCell.EnvironmentId:X8} not found");
if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null)
throw new InvalidOperationException($"CellStruct {datCell.CellStructure} missing from environment");
var world = WorldTransform(datCell);
cache.CacheCellStruct(cellId, datCell, cellStruct, world); // physics CellPhysics (real CellBSP)
return EnvCell.FromDat(cellId, datCell, cellStruct, world); // render/containment EnvCell (real ContainmentBsp)
}
}

View file

@ -0,0 +1,155 @@
using System;
using System.Linq;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// P0 Task 2 — characterize the real Holtburg cottage neighborhood from the
/// dats so the rest of P0 pins golden outcomes against verified cell ids,
/// not the master plan's loose "0031↔0170↔0171". The cottage cellar is the
/// 0x014x range (#98 fixtures); the doorway room/vestibule is in 0x017x.
/// </summary>
public class CottageDoorwayCharacterizationTests
{
private readonly ITestOutputHelper _out;
public CottageDoorwayCharacterizationTests(ITestOutputHelper output) => _out = output;
[Fact]
public void Characterize_CottageNeighborhood_PrintStructure()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
// Scan the cellar (014x) + intermediate (015x/016x) + doorway (017x)
// indoor cell range in landblock 0xA9B4. Print every cell that loads.
for (uint low = 0x0140; low <= 0x017F; low++)
{
uint id = ConformanceDats.HoltburgLandblock | low;
var cache = new PhysicsDataCache();
try
{
var cell = ConformanceDats.LoadEnvCell(dats, cache, id);
var phys = cache.GetCellStruct(id)!;
var origin = Vector3.Transform(Vector3.Zero, phys.WorldTransform);
bool exit = phys.Portals.Any(p => p.OtherCellId == 0xFFFFu);
_out.WriteLine(
$"0x{id:X8}: origin=({origin.X,7:F2},{origin.Y,7:F2},{origin.Z,6:F2}) " +
$"seenOut={(cell.SeenOutside ? 1 : 0)} bsp={(cell.ContainmentBsp?.Root is not null ? 1 : 0)} " +
$"portals={phys.Portals.Count} exit={(exit ? 1 : 0)} stab={cell.StabList.Count} " +
$"dests=[{string.Join(",", phys.Portals.Select(p => $"0x{p.OtherCellId:X4}"))}]");
}
catch (Exception ex)
{
// Most ids in the range won't exist — that's expected; skip silently
// unless it's an unexpected failure shape.
if (ex is not InvalidOperationException)
_out.WriteLine($"0x{id:X8}: ERROR {ex.GetType().Name}: {ex.Message}");
}
}
}
// The verified threshold building (origin 161.93,7.50,94.00):
// 0xA9B40170 vestibule (exit portal 0xFFFF + portal to 0171)
// 0xA9B40171 room (portals to 0170/0173/0175)
// Outdoor landcell at that XY = 0xA9B40031 (grid 6,0).
public const uint Vestibule0170 = 0xA9B40170u;
public const uint Room0171 = 0xA9B40171u;
public const uint OutdoorLandcell0031 = 0xA9B40031u;
[Fact]
public void Characterize_Doorway_FindInteriorPoints()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
foreach (var id in new[] { Vestibule0170, Room0171 })
{
var cache = new PhysicsDataCache();
var cell = ConformanceDats.LoadEnvCell(dats, cache, id);
var phys = cache.GetCellStruct(id)!;
_out.WriteLine($"0x{id:X8}: localBounds min=({cell.LocalBoundsMin.X:F2},{cell.LocalBoundsMin.Y:F2},{cell.LocalBoundsMin.Z:F2}) " +
$"max=({cell.LocalBoundsMax.X:F2},{cell.LocalBoundsMax.Y:F2},{cell.LocalBoundsMax.Z:F2})");
// Probe a 5×5×5 grid inset 15% from the bounds for the first cell-LOCAL
// point whose PointInCell (world) is true. That point is the golden interior.
var min = cell.LocalBoundsMin; var max = cell.LocalBoundsMax;
int inside = 0; Vector3? firstInsideLocal = null;
for (int ix = 1; ix <= 5; ix++)
for (int iy = 1; iy <= 5; iy++)
for (int iz = 1; iz <= 5; iz++)
{
var local = new Vector3(
min.X + (max.X - min.X) * ix / 6f,
min.Y + (max.Y - min.Y) * iy / 6f,
min.Z + (max.Z - min.Z) * iz / 6f);
var world = Vector3.Transform(local, phys.WorldTransform);
if (cell.PointInCell(world)) { inside++; firstInsideLocal ??= local; }
}
_out.WriteLine($" insidePoints={inside}/125 firstInsideLocal=" +
(firstInsideLocal is { } p
? $"({p.X:F3},{p.Y:F3},{p.Z:F3}) world={Vector3.Transform(p, phys.WorldTransform):F3}"
: "NONE"));
}
}
// ── Pinned regression guards (the characterized facts) ───────────────
// Verified interior points (cell-LOCAL), characterized above:
public static readonly Vector3 Interior0170Local = new(5.865f, -8.449f, 0.417f);
public static readonly Vector3 Interior0171Local = new(6.55f, -3.25f, 4.60f); // bsphere origin
[Fact]
public void Doorway_Topology_IsPinned()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
var vestibule = ConformanceDats.LoadEnvCell(dats, cache, Vestibule0170);
var room = ConformanceDats.LoadEnvCell(dats, cache, Room0171);
var vPhys = cache.GetCellStruct(Vestibule0170)!;
var rPhys = cache.GetCellStruct(Room0171)!;
Assert.True(vestibule.ContainmentBsp?.Root is not null, "vestibule must have a real BSP");
Assert.True(room.ContainmentBsp?.Root is not null, "room must have a real BSP");
Assert.True(vestibule.SeenOutside);
Assert.True(room.SeenOutside);
// Vestibule 0170: exit portal (0xFFFF) + portal to room 0171.
Assert.Contains(vPhys.Portals, p => p.OtherCellId == 0xFFFFu);
Assert.Contains(vPhys.Portals, p => p.OtherCellId == 0x0171u);
// Room 0171: portals to vestibule + the two side rooms; NO exit portal.
Assert.Contains(rPhys.Portals, p => p.OtherCellId == 0x0170u);
Assert.DoesNotContain(rPhys.Portals, p => p.OtherCellId == 0xFFFFu);
}
[Fact]
public void Doorway_InteriorPoints_ArePinned()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = new PhysicsDataCache();
var vestibule = ConformanceDats.LoadEnvCell(dats, cache, Vestibule0170);
var room = ConformanceDats.LoadEnvCell(dats, cache, Room0171);
var vWorld = Vector3.Transform(Interior0170Local, cache.GetCellStruct(Vestibule0170)!.WorldTransform);
var rWorld = Vector3.Transform(Interior0171Local, cache.GetCellStruct(Room0171)!.WorldTransform);
Assert.True(vestibule.PointInCell(vWorld), $"pinned vestibule interior {vWorld} must be inside 0170");
Assert.True(room.PointInCell(rWorld), $"pinned room interior {rWorld} must be inside 0171");
// The room interior is NOT inside the vestibule (distinct cells).
Assert.False(vestibule.PointInCell(rWorld), "room interior must not be inside the vestibule");
Assert.False(room.PointInCell(new Vector3(10000f, 10000f, 10000f)), "10km-away point cannot be inside");
}
}