diff --git a/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs b/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs new file mode 100644 index 0000000..8d09c0c --- /dev/null +++ b/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs @@ -0,0 +1,87 @@ +using System.Numerics; +using AcDream.Core.Physics; +using DatReaderWriter; +using DatReaderWriter.Options; +using Xunit; + +namespace AcDream.Core.Tests.Conformance; + +/// +/// P0 Task 4 — golden conformance for the membership pick +/// (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). +/// +public class FindCellListConformanceTests +{ + private const float FootRadius = 0.4f; // player foot-sphere radius + + /// Cache the whole threshold building (016F..0175) so portal expansion is faithful. + 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); + } + + /// + /// 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. + /// + [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); + } +}