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
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