using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Guards the DAT-only shape-source rule (Slice 1 of the unified /// collision-inclusion phase, 2026-06-24). /// /// /// Retail oracle: CPartArray::InitParts@0x00517F40 (parts come from /// the Setup list only), CGfxObj::Serialize@0x00534970 (physics_bsp /// gated on serialized-flags bit-0), CPhysicsPart::find_obj_collisions@0x0050D8D0 /// (returns OK / passable when physics_bsp == null). Render-mesh bounds /// never enter collision. /// /// public class ShadowShapeBuilderShapeSourceTests { // Retail: an object with no DAT physics shape (no CylSphere, no Sphere, // no part with a PhysicsBSP) emits NO collision shape and is passable. // CPhysicsPart::find_obj_collisions@0x0050D8D0 returns OK when physics_bsp==null. [Fact] public void Setup_WithNoCylSpheres_NoSpheres_NoPhysicsBspParts_YieldsEmptyShapeList() { var setup = new Setup { CylSpheres = new List(), Spheres = new List(), Parts = { 0x01000ABCu }, PlacementFrames = new Dictionary(), }; var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f, hasPhysicsBsp: _ => false); Assert.Empty(shapes); } // D4+W1 (2026-06-24) — BSP-only furniture weenie guard. // // A Setup with no CylSpheres, no Spheres, no Radius, but whose Part has a // PhysicsBSP (e.g. a candle holder or floor candelabra) MUST produce a BSP // ShadowShape. The premature `if (!hasCyl && !hasSphere && !hasRadius) return` // gate in GameWindow.RegisterLiveEntityCollision was dropped because it fired // BEFORE ShadowShapeBuilder ran and discarded these entities entirely. // // Retail oracle: CPhysicsPart::find_obj_collisions@0x0050D8D0 -- when // physics_bsp is non-null the part IS tested; the outer loop in // CPartArray::FindObjCollisions iterates all parts regardless of // CylSpheres/Spheres. ShadowShapeBuilder.FromSetup mirrors this by emitting // one BSP shape per part that `hasPhysicsBsp` returns true for. [Fact] public void Setup_WithBspPart_NoCylSpheres_EmitsBspShape() { // hasPhysicsBsp predicate returns true for 0x0100AAAAu, simulating a // GfxObj that has a PhysicsBSP but no CylSpheres/Spheres. const uint BspGfxObjId = 0x0100AAAAu; var setup = new Setup { CylSpheres = new List(), Spheres = new List(), Parts = { BspGfxObjId }, PlacementFrames = new Dictionary(), }; var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f, hasPhysicsBsp: id => id == BspGfxObjId); Assert.Contains(shapes, s => s.CollisionType == ShadowCollisionType.BSP); } // Complementary regression guard: a Setup with no CylSpheres, no Spheres, // and a Part that has NO PhysicsBSP must still produce an empty shape list. // The premature gate removal does not accidentally register shapeless entities. [Fact] public void Setup_WithPartButNoBsp_NoCylSpheres_YieldsEmptyShapeList() { const uint NoBspGfxObjId = 0x0100BBBBu; var setup = new Setup { CylSpheres = new List(), Spheres = new List(), Parts = { NoBspGfxObjId }, PlacementFrames = new Dictionary(), }; var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f, hasPhysicsBsp: _ => false); Assert.Empty(shapes); } // 2026-07-07 — the PREMISE of the player-vs-monster crowd-collision fix (the // CSphere family port): a humanoid Setup carries body Spheres and NO // CylSpheres, so FromSetup emits Sphere-type shadows → collision runs through // Transition.SphereCollision (the retail CSphere::intersects_sphere family), // NOT the cylinder path. Mirrors the human Setup 0x02000001 (two body spheres // at 0.475 / 1.350, r=0.48 — TS-46 / the physics digest). If this ever flips // to CylSphere-first, the crowd fix would be aimed at the wrong dispatcher. [Fact] public void Setup_WithBodySpheres_NoCylSpheres_EmitsSphereShapes() { var setup = new Setup { CylSpheres = new List(), Spheres = new List { new Sphere { Origin = new Vector3(0f, 0f, 0.475f), Radius = 0.48f }, new Sphere { Origin = new Vector3(0f, 0f, 1.350f), Radius = 0.48f }, }, Parts = new List>(), PlacementFrames = new Dictionary(), }; var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f, hasPhysicsBsp: _ => false); Assert.Equal(2, shapes.Count); Assert.All(shapes, s => Assert.Equal(ShadowCollisionType.Sphere, s.CollisionType)); } }