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,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");
}
}