using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// A6.P7 (2026-05-25) — retail-binary dispatch rule. Retail's /// CPhysicsObj::FindObjCollisions at /// docs/research/named-retail/acclient_2013_pseudo_c.txt:276861 /// dispatches BINARILY between "BSP-only" and "cyl + sphere" based on /// the HAS_PHYSICS_BSP_PS flag (bit 16, hex 0x10000) in /// PhysicsState. The flag is defined in /// docs/research/named-retail/acclient.h:2833 and mirrored in /// ACE at references/ACE/Source/ACE.Entity/Enum/PhysicsState.cs:24 /// as HasPhysicsBSP = 0x00010000. /// /// /// After the whole-object PvP and missile exemption gates, an /// entity with HAS_PHYSICS_BSP_PS in its state tests its BSP exclusively /// — the foot cyl is NEVER tested. The closed cottage door (state /// 0x10008 = STATIC | REPORT_COLLISIONS | HAS_PHYSICS_BSP) /// hits this branch. /// /// /// /// Pre-A6.P7: our dispatcher iterates every ShadowEntry /// independently and tests both the cyl AND the BSP for a door. The /// cyl's radial normal contaminates the slide direction at NE/SE /// approach headings (see /// docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md). /// /// /// /// Post-A6.P7: the dispatcher consults /// on each cyl/sphere /// entry and skips it when the entity has BSP, matching retail. /// /// public class A6P7DispatchRulesTests { /// /// Retail bit constant HAS_PHYSICS_BSP_PS = 0x10000 /// (acclient.h:2833) and ACE's HasPhysicsBSP = 0x00010000 /// (PhysicsState.cs:24) must match the value we expose on /// . /// [Fact] public void PhysicsStateFlags_HasPhysicsBsp_Is_Bit_16() { Assert.Equal(0x00010000u, (uint)PhysicsStateFlags.HasPhysicsBsp); } /// /// The dispatch predicate maps directly from retail's branch at /// acclient_2013_pseudo_c.txt:276861: /// /// ((state & 0x10000) == 0 || ebp_1 != 0 || eax_12 != 0) /// → cyl + sphere path /// else /// → BSP-only path /// /// The PvP and missile terms are whole-object early exits in the port. /// Once shape dispatch is reached, the predicate is therefore: /// "BSP-only iff HAS_PHYSICS_BSP_PS is set". /// [Theory] [InlineData(0x00010008u, true)] // closed cottage door — STATIC | REPORT | HAS_BSP [InlineData(0x00010000u, true)] // bare HAS_BSP [InlineData(0x00110000u, true)] // HAS_BSP | CLOAKED [InlineData(0x00000008u, false)] // creature — REPORT_COLLISIONS only [InlineData(0x00000000u, false)] // empty state [InlineData(0x0000FFFFu, false)] // every bit BELOW 0x10000 set public void BspOnlyDispatch_RespectsHasPhysicsBspFlag( uint entityState, bool expected) { Assert.Equal(expected, Transition.BspOnlyDispatch(entityState)); } /// /// A non-missile mover does not trigger the retail missile exemption. /// Retail oracle: OBJECTINFO::missile_ignore pc:274385; /// dispatch use pc:276858–276861. /// [Fact] public void NonMissileMover_DoesNotIgnoreOrdinaryTarget() { var mover = new ObjectInfo(); Assert.False(mover.MissileIgnore( targetEntityId: 1u, targetPhysicsState: 0u, targetFlags: EntityCollisionFlags.HasWeenie)); } /// /// Guard: a BSP-flagged entity that reaches shape dispatch still uses /// only its BSP — A6.P7 door behavior remains unchanged. /// This protects the A6.P7 cottage-door / dungeon-wall fix. /// [Fact] public void BspOnlyDispatch_DoorStateStillDispatchesBspOnly() { // Cottage door state from A6.P7 investigation: 0x10008 = STATIC | REPORT | HAS_BSP const uint doorState = 0x00010008u; Assert.True(Transition.BspOnlyDispatch(doorState)); } }