test(p0): find_cell_list golden conformance — unambiguous interior picks

P0 Task 4. FindCellList resolves a sphere deep inside room 0171 -> 0171,
deep inside vestibule 0170 -> 0170, and re-picks 0170 from a stale 0171
seed (membership re-picks by containment, not the seed). Retail-faithful
by construction (candidate cells loaded from the real dats). The subtle
doorway-threshold pick is the trace-backed golden (Task 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-03 14:21:44 +02:00
parent a90f34368f
commit ec78beb843

View file

@ -0,0 +1,87 @@
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// P0 Task 4 — golden conformance for the membership pick
/// <see cref="CellTransit.FindCellList"/> (retail CObjCell::find_cell_list
/// @ 0x52b4e0 pc:308742). The unambiguous cases are retail-faithful by
/// construction: the candidate cells are loaded from the same dats retail
/// loads, and a sphere clearly inside one room can only resolve to that
/// room. The subtle doorway-threshold pick (the 0031↔0170↔0171 ping-pong)
/// is the trace-backed golden in FindCellListConformanceTests (Task 6).
/// </summary>
public class FindCellListConformanceTests
{
private const float FootRadius = 0.4f; // player foot-sphere radius
/// <summary>Cache the whole threshold building (016F..0175) so portal expansion is faithful.</summary>
private static PhysicsDataCache LoadThresholdBuilding(DatCollection dats)
{
var cache = new PhysicsDataCache();
for (uint low = 0x016Fu; low <= 0x0175u; low++)
ConformanceDats.LoadEnvCell(dats, cache, ConformanceDats.HoltburgLandblock | low);
return cache;
}
[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 = LoadThresholdBuilding(dats);
var world = Vector3.Transform(
CottageDoorwayCharacterizationTests.Interior0171Local,
cache.GetCellStruct(CottageDoorwayCharacterizationTests.Room0171)!.WorldTransform);
uint picked = CellTransit.FindCellList(cache, world, FootRadius,
CottageDoorwayCharacterizationTests.Room0171);
Assert.Equal(CottageDoorwayCharacterizationTests.Room0171, picked);
}
[Fact]
public void FindCellList_DeepInsideVestibule0170_Returns0170()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = LoadThresholdBuilding(dats);
var world = Vector3.Transform(
CottageDoorwayCharacterizationTests.Interior0170Local,
cache.GetCellStruct(CottageDoorwayCharacterizationTests.Vestibule0170)!.WorldTransform);
uint picked = CellTransit.FindCellList(cache, world, FootRadius,
CottageDoorwayCharacterizationTests.Vestibule0170);
Assert.Equal(CottageDoorwayCharacterizationTests.Vestibule0170, picked);
}
/// <summary>
/// Seeding from the WRONG current cell (room 0171) while standing deep inside
/// the vestibule (0170) must still resolve to 0170 — find_cell_list re-picks by
/// containment, it does not trust the stale seed. This is the membership-stability
/// property P1 must preserve.
/// </summary>
[Fact]
public void FindCellList_InVestibule_SeededFromRoom_Returns0170()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cache = LoadThresholdBuilding(dats);
var world = Vector3.Transform(
CottageDoorwayCharacterizationTests.Interior0170Local,
cache.GetCellStruct(CottageDoorwayCharacterizationTests.Vestibule0170)!.WorldTransform);
uint picked = CellTransit.FindCellList(cache, world, FootRadius,
CottageDoorwayCharacterizationTests.Room0171); // stale/wrong seed
Assert.Equal(CottageDoorwayCharacterizationTests.Vestibule0170, picked);
}
}