using System; using System.Collections.Generic; 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); } }