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:
parent
a859116d5f
commit
a90f34368f
4 changed files with 1000 additions and 0 deletions
70
docs/research/2026-06-03-p0-conformance-apparatus-notes.md
Normal file
70
docs/research/2026-06-03-p0-conformance-apparatus-notes.md
Normal 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.
|
||||
709
docs/superpowers/plans/2026-06-03-p0-conformance-apparatus.md
Normal file
709
docs/superpowers/plans/2026-06-03-p0-conformance-apparatus.md
Normal 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 5–10×); 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.
|
||||
66
tests/AcDream.Core.Tests/Conformance/ConformanceDats.cs
Normal file
66
tests/AcDream.Core.Tests/Conformance/ConformanceDats.cs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue