using System.Numerics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Unit tests for . /// /// Real BSP data requires dat files (integration-test territory), so these /// tests use manually constructed BSP nodes and polygon/vertex data that /// match the structure the dat reader would produce. /// public class BSPQueryTests { // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- /// /// Build a with four vertices forming a unit /// square in the XY-plane (Z = 0), ids 0-3. /// private static VertexArray UnitSquareVertexArray() { var va = new VertexArray(); var positions = new[] { new Vector3(0f, 0f, 0f), new Vector3(1f, 0f, 0f), new Vector3(1f, 1f, 0f), new Vector3(0f, 1f, 0f), }; for (ushort i = 0; i < positions.Length; i++) { var sv = new SWVertex { Origin = positions[i], Normal = Vector3.UnitZ }; va.Vertices[i] = sv; } return va; } /// /// Build a referencing vertex ids 0-3 in order. /// private static Polygon UnitSquarePolygon() => new Polygon { SidesType = DatReaderWriter.Enums.CullMode.None, VertexIds = new List { 0, 1, 2, 3 }, }; /// /// Build a leaf containing one polygon (id 0) /// with a bounding sphere that covers the unit square. /// private static PhysicsBSPNode LeafNode(Sphere bounds) { var node = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = bounds, }; node.Polygons.Add(0); return node; } // ----------------------------------------------------------------------- // Test 1: null node returns false without throwing // ----------------------------------------------------------------------- [Fact] public void SphereIntersectsPoly_NullNode_ReturnsFalse() { var polygons = new Dictionary(); var vertices = new VertexArray(); bool hit = BSPQuery.SphereIntersectsPoly( null, polygons, vertices, new Vector3(0.5f, 0.5f, 0.1f), 0.2f, out _, out _); Assert.False(hit); } // ----------------------------------------------------------------------- // Test 2: sphere far outside the bounding sphere is fast-rejected // ----------------------------------------------------------------------- [Fact] public void SphereIntersectsPoly_MissesBoundingSphere_ReturnsFalse() { // Leaf node centred at origin with radius 1. var bounds = new Sphere { Origin = Vector3.Zero, Radius = 1f }; var node = LeafNode(bounds); var polygons = new Dictionary { [0] = UnitSquarePolygon() }; var vertices = UnitSquareVertexArray(); // Sphere is 100 units away — broad phase must reject. bool hit = BSPQuery.SphereIntersectsPoly( node, polygons, vertices, new Vector3(100f, 100f, 100f), 0.5f, out _, out _); Assert.False(hit); } // ----------------------------------------------------------------------- // Test 3: sphere resting just above the unit-square floor polygon hits // ----------------------------------------------------------------------- [Fact] public void SphereIntersectsPoly_HitsLeafPolygon() { // Bounding sphere covers the 1×1 unit-square leaf. var bounds = new Sphere { Origin = new Vector3(0.5f, 0.5f, 0f), Radius = 2f }; var node = LeafNode(bounds); var polygons = new Dictionary { [0] = UnitSquarePolygon() }; var vertices = UnitSquareVertexArray(); // Sphere centred at (0.5, 0.5, 0.3) with radius 0.5 should touch Z=0 plane. bool hit = BSPQuery.SphereIntersectsPoly( node, polygons, vertices, new Vector3(0.5f, 0.5f, 0.3f), 0.5f, out ushort polyId, out Vector3 normal); Assert.True(hit); Assert.Equal(0, polyId); // Normal should point roughly upward (+Z). Assert.True(normal.Z > 0.9f, $"Expected Z-up normal, got {normal}"); } // ----------------------------------------------------------------------- // Test 4: sphere entirely above the polygon (no contact) returns false // ----------------------------------------------------------------------- [Fact] public void SphereIntersectsPoly_SphereTooHigh_ReturnsFalse() { var bounds = new Sphere { Origin = new Vector3(0.5f, 0.5f, 0f), Radius = 2f }; var node = LeafNode(bounds); var polygons = new Dictionary { [0] = UnitSquarePolygon() }; var vertices = UnitSquareVertexArray(); // Sphere centred 5 units above the floor with radius 0.3 → no contact. bool hit = BSPQuery.SphereIntersectsPoly( node, polygons, vertices, new Vector3(0.5f, 0.5f, 5f), 0.3f, out _, out _); Assert.False(hit); } // ----------------------------------------------------------------------- // Test 5: internal node — sphere on positive side recurses pos subtree // ----------------------------------------------------------------------- [Fact] public void SphereIntersectsPoly_InternalNode_PosSubtreeHit() { // Leaf on the positive side (Z > 0 half-space) contains the floor poly. var leafBounds = new Sphere { Origin = new Vector3(0.5f, 0.5f, 0f), Radius = 2f }; var leafNode = LeafNode(leafBounds); var polygons = new Dictionary { [0] = UnitSquarePolygon() }; var vertices = UnitSquareVertexArray(); // Splitting plane: Z = 0, normal = +Z, D = 0. // Sphere at Z = 0.3 is on the positive side. var internalBounds = new Sphere { Origin = new Vector3(0.5f, 0.5f, 0f), Radius = 5f }; var internalNode = new PhysicsBSPNode { Type = BSPNodeType.BPnn, // has PosNode only (BPnn = pos + null-neg) SplittingPlane = new Plane(Vector3.UnitZ, 0f), BoundingSphere = internalBounds, PosNode = leafNode, NegNode = null, }; bool hit = BSPQuery.SphereIntersectsPoly( internalNode, polygons, vertices, new Vector3(0.5f, 0.5f, 0.3f), 0.5f, out ushort polyId, out _); Assert.True(hit); Assert.Equal(0, polyId); } // ----------------------------------------------------------------------- // Test 6: PhysicsDataCache — caches GfxObj with physics data // ----------------------------------------------------------------------- [Fact] public void PhysicsDataCache_CachesGfxObjWithPhysics() { // We can't easily construct a real GfxObj (field-based dat type), // so this test verifies the SetupPhysics cache path which is more // easily instantiated. var cache = new PhysicsDataCache(); Assert.Equal(0, cache.GfxObjCount); Assert.Equal(0, cache.SetupCount); // GetGfxObj for an unknown id should return null safely. Assert.Null(cache.GetGfxObj(0x01000001u)); Assert.Null(cache.GetSetup(0x02000001u)); } }