using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Tests.Conformance; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using DatReaderWriter.Types; using Xunit.Abstractions; namespace AcDream.Core.Tests.Physics; public sealed class FlatCollisionInstalledDatTests { private readonly ITestOutputHelper _output; public FlatCollisionInstalledDatTests(ITestOutputHelper output) => _output = output; [Fact] public void InstalledCells_FlattenVerbatimAndDeterministically() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _output.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); foreach (uint cellId in new[] { 0x8A02_016Eu, // Facility Hub corridor 0x8A02_017Au, // asymmetric adjoining corridor 0xA9B4_013Fu, // Holtburg cottage 0xA9B4_0150u, // Holtburg cottage exterior vestibule 0xA9B4_0159u, // Holtburg inn south room 0xA9B4_015Au, // Holtburg inn ground-floor connector 0xA9B4_0161u, // Holtburg inn upper-floor connector 0xA9B4_0162u, // Holtburg inn main room / stairwell 0xA9B4_0164u, // Holtburg inn exterior vestibule 0xA9B4_0166u, // Holtburg inn upper floor }) { var cache = new PhysicsDataCache(); ConformanceDats.LoadEnvCell(dats, cache, cellId); CellPhysics source = Assert.IsType( cache.GetCellStruct(cellId)); FlatCellCollisionAsset first = FlatCollisionAssetBuilder.FlattenCell(source); FlatCellCollisionAsset second = FlatCollisionAssetBuilder.FlattenCell(source); EnvCell datCell = Assert.IsType( dats.Get(cellId)); var environment = Assert.IsType( dats.Get( 0x0D00_0000u | datCell.EnvironmentId)); Assert.True( environment.Cells.TryGetValue( datCell.CellStructure, out CellStruct? cellStruct)); Assert.NotNull(cellStruct); FlatCellStructureCollisionAsset directStructure = FlatCollisionAssetBuilder.FlattenCellStructure(cellStruct!); FlatEnvCellTopology directTopology = FlatCollisionAssetBuilder.FlattenEnvCellTopology( cellId, datCell, directStructure.PortalPolygons); AssertPhysicsSourceMatches(source.BSP?.Root, source.Resolved, first.Structure.PhysicsBsp); AssertContainmentSourceMatches( source.CellBSP?.Root, first.Structure.ContainmentBsp); FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual( first.Structure.PhysicsBsp, second.Structure.PhysicsBsp); Assert.Equal( first.Structure.ContainmentBsp.Nodes.AsEnumerable(), second.Structure.ContainmentBsp.Nodes.AsEnumerable()); Assert.Equal( first.Topology.Portals.AsEnumerable(), second.Topology.Portals.AsEnumerable()); Assert.Equal( first.Topology.VisibleCellIds.AsEnumerable(), second.Topology.VisibleCellIds.AsEnumerable()); FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual( first.Structure.PhysicsBsp, directStructure.PhysicsBsp); Assert.Equal( first.Structure.ContainmentBsp.Nodes.AsEnumerable(), directStructure.ContainmentBsp.Nodes.AsEnumerable()); AssertPolygonTableEqual( first.Structure.PortalPolygons, directStructure.PortalPolygons); Assert.Equal( first.Topology.Portals.AsEnumerable(), directTopology.Portals.AsEnumerable()); Assert.Equal( first.Topology.VisibleCellIds.AsEnumerable(), directTopology.VisibleCellIds.AsEnumerable()); Assert.Equal(first.Topology.SeenOutside, directTopology.SeenOutside); _output.WriteLine( $"cell 0x{cellId:X8}: physicsNodes={first.Structure.PhysicsBsp.Nodes.Length}, " + $"physicsPolygons={first.Structure.PhysicsBsp.PolygonTable.Polygons.Length}, " + $"containmentNodes={first.Structure.ContainmentBsp.Nodes.Length}, " + $"portals={first.Topology.Portals.Length}"); } } [Fact] public void InstalledGfxObjsAndSetups_FlattenVerbatim() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _output.WriteLine("SKIP: installed retail DAT directory is unavailable."); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = new PhysicsDataCache(); foreach (uint gfxObjId in new[] { 0x0100_0A2Bu, // Holtburg cottage shell 0x0100_0C17u, // Holtburg inn shell 0x0100_0AC5u, // outdoor stair/ramp fixture 0x0100_44B5u, // multipart door shell }) { GfxObj gfxObj = Assert.IsType(dats.Get(gfxObjId)); cache.CacheGfxObj(gfxObjId, gfxObj); GfxObjPhysics source = Assert.IsType( cache.GetGfxObj(gfxObjId)); FlatGfxObjCollisionAsset flat = FlatCollisionAssetBuilder.FlattenGfxObj( source, cache.GetVisualBounds(gfxObjId)); FlatGfxObjCollisionAsset direct = FlatCollisionAssetBuilder.FlattenGfxObj(gfxObj); AssertPhysicsSourceMatches( source.BSP.Root, source.Resolved, flat.PhysicsBsp); AssertNullableSphereBits(source.BoundingSphere, flat.BoundingSphere); Assert.NotNull(flat.VisualBounds); AssertVisualBoundsBits( Assert.IsType(cache.GetVisualBounds(gfxObjId)), flat.VisualBounds!.Value); FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual( flat.PhysicsBsp, direct.PhysicsBsp); AssertFlatNullableSphereBits( flat.BoundingSphere, direct.BoundingSphere); AssertFlatNullableVisualBoundsBits( flat.VisualBounds, direct.VisualBounds); _output.WriteLine( $"gfx 0x{gfxObjId:X8}: nodes={flat.PhysicsBsp.Nodes.Length}, " + $"polygons={flat.PhysicsBsp.PolygonTable.Polygons.Length}"); } foreach (uint setupId in new[] { 0x0200_0001u, // player 0x0200_19E3u, // wide low collision family 0x0200_19FFu, // multipart door }) { Setup setup = Assert.IsType(dats.Get(setupId)); cache.CacheSetup(setupId, setup); SetupPhysics source = Assert.IsType( cache.GetSetup(setupId)); FlatSetupCollision flat = FlatCollisionAssetBuilder.FlattenSetup(source); FlatSetupCollision direct = FlatCollisionAssetBuilder.FlattenSetup(setup); AssertSetupSourceMatches(source, flat); AssertSetupFlatEqual(flat, direct); _output.WriteLine( $"setup 0x{setupId:X8}: cylinders={flat.Cylinders.Length}, " + $"spheres={flat.Spheres.Length}"); } } private static void AssertPhysicsSourceMatches( PhysicsBSPNode? root, IReadOnlyDictionary resolved, FlatPhysicsBsp flat) { if (root is null) { Assert.Equal(-1, flat.RootIndex); Assert.Empty(flat.Nodes); return; } var ordered = new List(); var indices = new Dictionary( ReferenceEqualityComparer.Instance); var stack = new Stack(); stack.Push(root); while (stack.Count != 0) { PhysicsBSPNode source = stack.Pop(); indices.Add(source, ordered.Count); ordered.Add(source); if (source.NegNode is not null) stack.Push(source.NegNode); if (source.PosNode is not null) stack.Push(source.PosNode); } Assert.Equal(flat.Nodes.Length, ordered.Count); for (int flatIndex = 0; flatIndex < ordered.Count; flatIndex++) { PhysicsBSPNode source = ordered[flatIndex]; FlatPhysicsBspNode row = flat.Nodes[flatIndex]; Assert.Equal(source.Type, row.Type); FlatCollisionAssetBuilderTests.AssertPlaneBits( source.SplittingPlane, row.SplittingPlane); Assert.Equal(source.LeafIndex, row.LeafIndex); Assert.Equal(source.Solid, row.Solid); FlatCollisionAssetBuilderTests.AssertVectorBits( source.BoundingSphere.Origin, row.BoundingSphere.Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( source.BoundingSphere.Radius, row.BoundingSphere.Radius); Assert.Equal( source.PosNode is null ? -1 : indices[source.PosNode], row.PositiveChildIndex); Assert.Equal( source.NegNode is null ? -1 : indices[source.NegNode], row.NegativeChildIndex); Assert.Equal(source.Polygons.Count, row.PolygonIndexRange.Count); for (int i = 0; i < source.Polygons.Count; i++) { int streamIndex = row.PolygonIndexRange.Start + i; int polygonIndex = flat.PolygonIndexStream[streamIndex]; Assert.Equal( source.Polygons[i], flat.PolygonTable.Polygons[polygonIndex].Id); } } Assert.Equal(resolved.Count, flat.PolygonTable.Polygons.Length); foreach ((ushort id, ResolvedPolygon source) in resolved) { Assert.True(flat.PolygonTable.TryFindPolygonIndex(id, out int polygonIndex)); FlatCollisionPolygon row = flat.PolygonTable.Polygons[polygonIndex]; Assert.Equal(source.Id, row.Id); Assert.Equal(source.NumPoints, row.NumPoints); Assert.Equal(source.SidesType, row.SidesType); FlatCollisionAssetBuilderTests.AssertPlaneBits(source.Plane, row.Plane); for (int i = 0; i < source.Vertices.Length; i++) { FlatCollisionAssetBuilderTests.AssertVectorBits( source.Vertices[i], flat.PolygonTable.Vertices[row.VertexRange.Start + i]); } } } private static void AssertContainmentSourceMatches( CellBSPNode? root, FlatCellContainmentBsp flat) { if (root is null) { Assert.Equal(-1, flat.RootIndex); Assert.Empty(flat.Nodes); return; } var ordered = new List(); var indices = new Dictionary( ReferenceEqualityComparer.Instance); var stack = new Stack(); stack.Push(root); while (stack.Count != 0) { CellBSPNode source = stack.Pop(); indices.Add(source, ordered.Count); ordered.Add(source); if (source.NegNode is not null) stack.Push(source.NegNode); if (source.PosNode is not null) stack.Push(source.PosNode); } Assert.Equal(flat.Nodes.Length, ordered.Count); for (int flatIndex = 0; flatIndex < ordered.Count; flatIndex++) { CellBSPNode source = ordered[flatIndex]; FlatCellBspNode row = flat.Nodes[flatIndex]; Assert.Equal(source.Type, row.Type); Assert.Equal(source.LeafIndex, row.LeafIndex); FlatCollisionAssetBuilderTests.AssertPlaneBits( source.SplittingPlane, row.SplittingPlane); Assert.Equal( source.PosNode is null ? -1 : indices[source.PosNode], row.PositiveChildIndex); Assert.Equal( source.NegNode is null ? -1 : indices[source.NegNode], row.NegativeChildIndex); } } private static void AssertSetupSourceMatches( SetupPhysics source, FlatSetupCollision flat) { Assert.Equal(source.CylSpheres.Count, flat.Cylinders.Length); Assert.Equal(source.Spheres.Count, flat.Spheres.Length); for (int i = 0; i < source.CylSpheres.Count; i++) { FlatCollisionAssetBuilderTests.AssertVectorBits( source.CylSpheres[i].Origin, flat.Cylinders[i].Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( source.CylSpheres[i].Radius, flat.Cylinders[i].Radius); FlatCollisionAssetBuilderTests.AssertFloatBits( source.CylSpheres[i].Height, flat.Cylinders[i].Height); } for (int i = 0; i < source.Spheres.Count; i++) { FlatCollisionAssetBuilderTests.AssertVectorBits( source.Spheres[i].Origin, flat.Spheres[i].Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( source.Spheres[i].Radius, flat.Spheres[i].Radius); } FlatCollisionAssetBuilderTests.AssertFloatBits(source.Height, flat.Height); FlatCollisionAssetBuilderTests.AssertFloatBits(source.Radius, flat.Radius); FlatCollisionAssetBuilderTests.AssertFloatBits( source.StepUpHeight, flat.StepUpHeight); FlatCollisionAssetBuilderTests.AssertFloatBits( source.StepDownHeight, flat.StepDownHeight); } private static void AssertNullableSphereBits( Sphere? source, FlatCollisionSphere? flat) { Assert.Equal(source is null, flat is null); if (source is null || flat is null) return; FlatCollisionAssetBuilderTests.AssertVectorBits( source.Origin, flat.Value.Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( source.Radius, flat.Value.Radius); } private static void AssertVisualBoundsBits( GfxObjVisualBounds source, FlatGfxObjVisualBounds flat) { FlatCollisionAssetBuilderTests.AssertVectorBits(source.Min, flat.Min); FlatCollisionAssetBuilderTests.AssertVectorBits(source.Max, flat.Max); FlatCollisionAssetBuilderTests.AssertVectorBits(source.Center, flat.Center); FlatCollisionAssetBuilderTests.AssertFloatBits(source.Radius, flat.Radius); FlatCollisionAssetBuilderTests.AssertVectorBits( source.HalfExtents, flat.HalfExtents); } private static void AssertFlatNullableSphereBits( FlatCollisionSphere? expected, FlatCollisionSphere? actual) { Assert.Equal(expected.HasValue, actual.HasValue); if (!expected.HasValue || !actual.HasValue) return; FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Value.Origin, actual.Value.Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Value.Radius, actual.Value.Radius); } private static void AssertFlatNullableVisualBoundsBits( FlatGfxObjVisualBounds? expected, FlatGfxObjVisualBounds? actual) { Assert.Equal(expected.HasValue, actual.HasValue); if (!expected.HasValue || !actual.HasValue) return; FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Value.Min, actual.Value.Min); FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Value.Max, actual.Value.Max); FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Value.Center, actual.Value.Center); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Value.Radius, actual.Value.Radius); FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Value.HalfExtents, actual.Value.HalfExtents); } private static void AssertSetupFlatEqual( FlatSetupCollision expected, FlatSetupCollision actual) { Assert.Equal(expected.Cylinders.Length, actual.Cylinders.Length); Assert.Equal(expected.Spheres.Length, actual.Spheres.Length); for (int i = 0; i < expected.Cylinders.Length; i++) { FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Cylinders[i].Origin, actual.Cylinders[i].Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Cylinders[i].Radius, actual.Cylinders[i].Radius); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Cylinders[i].Height, actual.Cylinders[i].Height); } for (int i = 0; i < expected.Spheres.Length; i++) { FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Spheres[i].Origin, actual.Spheres[i].Origin); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Spheres[i].Radius, actual.Spheres[i].Radius); } FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Height, actual.Height); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.Radius, actual.Radius); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.StepUpHeight, actual.StepUpHeight); FlatCollisionAssetBuilderTests.AssertFloatBits( expected.StepDownHeight, actual.StepDownHeight); } private static void AssertPolygonTableEqual( FlatPolygonTable expected, FlatPolygonTable actual) { Assert.Equal(expected.Polygons.Length, actual.Polygons.Length); Assert.Equal(expected.Vertices.Length, actual.Vertices.Length); for (int i = 0; i < expected.Polygons.Length; i++) { Assert.Equal(expected.Polygons[i].Id, actual.Polygons[i].Id); Assert.Equal( expected.Polygons[i].SidesType, actual.Polygons[i].SidesType); Assert.Equal( expected.Polygons[i].NumPoints, actual.Polygons[i].NumPoints); Assert.Equal( expected.Polygons[i].VertexRange, actual.Polygons[i].VertexRange); FlatCollisionAssetBuilderTests.AssertPlaneBits( expected.Polygons[i].Plane, actual.Polygons[i].Plane); } for (int i = 0; i < expected.Vertices.Length; i++) { FlatCollisionAssetBuilderTests.AssertVectorBits( expected.Vertices[i], actual.Vertices[i]); } } }