using AcDream.Core.Items; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Unit tests for — Commit A of the /// 2026-04-29 live-entity collision port. Verifies retail-faithful /// conversion from PWD bitfield bits to the boolean collision-decision /// flags that FindObjCollisions consumes for the PvP exemption. /// /// Bit positions confirmed against: /// /// docs/research/named-retail/acclient_2013_pseudo_c.txt:406898-406918 /// (ACCWeenieObject::IsPKLite/IsPK/IsImpenetrable) — the /// retail client itself reads bits 25 / 5 / 21 of pwd._bitfield. /// docs/research/named-retail/acclient.h:6431-6463 /// (PublicWeenieDesc::BitfieldIndex) — names the bits: /// BF_PLAYER=0x8, BF_PLAYER_KILLER=0x20, /// BF_FREE_PKSTATUS=0x200000, BF_PKLITE_PKSTATUS=0x2000000. /// docs/research/named-retail/acclient_2013_pseudo_c.txt:441868-441890 /// (PublicWeenieDesc::SetPlayerKillerStatus) — confirms /// the writer maps PKStatusEnum values onto these exact bits. /// /// public class EntityCollisionFlagsTests { [Fact] public void FromPwdBitfield_AllZeros_NoFlags() { Assert.Equal(EntityCollisionFlags.None, EntityCollisionFlagsExt.FromPwdBitfield(0u)); } [Fact] public void FromPwdBitfield_PlayerBit_SetsIsPlayer() { // BF_PLAYER = 0x8 (bit 3) var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x8u); Assert.True(flags.HasFlag(EntityCollisionFlags.IsPlayer)); Assert.False(flags.HasFlag(EntityCollisionFlags.IsPK)); } [Fact] public void FromPwdBitfield_PlayerKillerBit_SetsIsPK() { // BF_PLAYER_KILLER = 0x20 (bit 5) var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x20u); Assert.True(flags.HasFlag(EntityCollisionFlags.IsPK)); Assert.False(flags.HasFlag(EntityCollisionFlags.IsPKLite)); } [Fact] public void FromPwdBitfield_PkLiteBit_SetsIsPKLite() { // BF_PKLITE_PKSTATUS = 0x2000000 (bit 25) var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x2000000u); Assert.True(flags.HasFlag(EntityCollisionFlags.IsPKLite)); Assert.False(flags.HasFlag(EntityCollisionFlags.IsPK)); } [Fact] public void FromPwdBitfield_FreePkStatusBit_SetsIsImpenetrable() { // BF_FREE_PKSTATUS = 0x200000 (bit 21) var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x200000u); Assert.True(flags.HasFlag(EntityCollisionFlags.IsImpenetrable)); } [Fact] public void FromPwdBitfield_PlayerAndPK_SetsBoth() { // A PK player: BF_PLAYER (0x8) | BF_PLAYER_KILLER (0x20) = 0x28 var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x28u); Assert.True(flags.HasFlag(EntityCollisionFlags.IsPlayer)); Assert.True(flags.HasFlag(EntityCollisionFlags.IsPK)); Assert.False(flags.HasFlag(EntityCollisionFlags.IsPKLite)); Assert.False(flags.HasFlag(EntityCollisionFlags.IsImpenetrable)); } [Fact] public void FromPwdBitfield_UnrelatedBits_Ignored() { // Set BF_OPENABLE (0x1), BF_INSCRIBABLE (0x2), BF_STUCK (0x4) — none // map to collision flags. A creature spawn might have BF_ATTACKABLE // (0x10) set; that's ItemType-derived IsCreature, not a PvP flag. var flags = EntityCollisionFlagsExt.FromPwdBitfield(0x1u | 0x2u | 0x4u | 0x10u); Assert.Equal(EntityCollisionFlags.None, flags); } // ── ToMoverState (TS-23, Campaign P Slice P3, 2026-07-30) ──────────────── [Fact] public void ToMoverState_None_ProducesNoneNotIsPlayer() { // ToMoverState deliberately does NOT translate IsPlayer — every // mover-flags call site derives ObjectInfoState.IsPlayer from its // own GUID-prefix heuristic. Confirms the OR is a true no-op for // the non-PK invariant. Assert.Equal(ObjectInfoState.None, EntityCollisionFlags.None.ToMoverState()); Assert.Equal( ObjectInfoState.None, EntityCollisionFlags.IsPlayer.ToMoverState()); } [Fact] public void ToMoverState_IsPK_TranslatesToObjectInfoStateIsPK() { Assert.Equal( ObjectInfoState.IsPK, EntityCollisionFlags.IsPK.ToMoverState()); } [Fact] public void ToMoverState_IsPKLite_TranslatesToObjectInfoStateIsPKLite() { Assert.Equal( ObjectInfoState.IsPKLite, EntityCollisionFlags.IsPKLite.ToMoverState()); } [Fact] public void ToMoverState_IsImpenetrable_TranslatesToObjectInfoStateIsImpenetrable() { Assert.Equal( ObjectInfoState.IsImpenetrable, EntityCollisionFlags.IsImpenetrable.ToMoverState()); } [Fact] public void ToMoverState_AllThreeBits_TranslateIndependently() { var flags = EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK | EntityCollisionFlags.IsPKLite | EntityCollisionFlags.IsImpenetrable | EntityCollisionFlags.IsCreature | EntityCollisionFlags.HasWeenie; ObjectInfoState state = flags.ToMoverState(); Assert.Equal( ObjectInfoState.IsPK | ObjectInfoState.IsPKLite | ObjectInfoState.IsImpenetrable, state); } /// /// End-to-end proof of the FULL PWD-bitfield-to-moverFlags pipeline /// (TS-23): a wire bitfield decodes through /// then /// to the exact /// bits every mover-flags call site now /// ORs into PhysicsEngine.ResolveWithTransition's moverFlags /// argument. /// [Fact] public void FromPwdBitfield_ThenToMoverState_PkPlayer_ProducesIsPKOnly() { // BF_PLAYER (0x8) | BF_PLAYER_KILLER (0x20). uint bitfield = 0x8u | 0x20u; ObjectInfoState moverState = EntityCollisionFlagsExt.FromPwdBitfield(bitfield).ToMoverState(); Assert.Equal(ObjectInfoState.IsPK, moverState); Assert.False(moverState.HasFlag(ObjectInfoState.IsPlayer)); } // ── ResolveMoverPvpState (TS-23) — the ClientObjectTable-backed lookup // every mover-flags call site (local player, remote sweep + teleport, // ordinary movers) now uses ──────────────────────────────────────────── private const uint PkGuid = 0x50000101u; private const uint NonPkGuid = 0x50000102u; /// /// THE non-PK invariant the plan requires: an entity with no row at /// all, and a row whose PublicWeenieBitfield is explicitly 0 (received /// but carries none of the tracked bits), BOTH resolve to /// — a no-op OR into moverFlags, /// bit-identical to the pre-P3 hardcoded value for every ACE /// default-created character. /// [Fact] public void ResolveMoverPvpState_NoRowOrZeroBitfield_IsNoneNotJustAbsentIsPlayer() { var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = NonPkGuid, PublicWeenieBitfield = 0u, }); // No row registered at all for this guid. Assert.Equal( ObjectInfoState.None, EntityCollisionFlagsExt.ResolveMoverPvpState(objects, 0x50000999u)); // A row exists, but PublicWeenieBitfield is 0 — no PK-relevant bits. Assert.Equal( ObjectInfoState.None, EntityCollisionFlagsExt.ResolveMoverPvpState(objects, NonPkGuid)); } /// /// A row whose is /// null (parsed CreateObject never carried the trailer field at /// all) is the SAME safe default — matches /// ClientObject.PublicWeenieBitfield's own "not yet known" /// sentinel. /// [Fact] public void ResolveMoverPvpState_NullBitfield_IsNone() { var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = NonPkGuid, PublicWeenieBitfield = null, }); Assert.Equal( ObjectInfoState.None, EntityCollisionFlagsExt.ResolveMoverPvpState(objects, NonPkGuid)); } /// /// The actual acceptance criterion (not just the non-regression guard): /// a PK/PKLite/Impenetrable row resolves to the exact matching /// ObjectInfoState bits. /// [Fact] public void ResolveMoverPvpState_PkPlayerRow_ResolvesToIsPK() { var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = PkGuid, // BF_PLAYER (0x8) | BF_PLAYER_KILLER (0x20). PublicWeenieBitfield = 0x8u | 0x20u, }); Assert.Equal( ObjectInfoState.IsPK, EntityCollisionFlagsExt.ResolveMoverPvpState(objects, PkGuid)); } /// /// End-to-end: two PK players' resolved moverFlags/targetFlags make /// CollisionExemption.ShouldSkip COLLIDE (retail: both-PK pairs /// collide), while a PK-vs-non-PK pair stays EXEMPT (walks through) — /// the exact acceptance criterion from the plan, now driven through the /// real ClientObjectTable-backed lookup instead of directly-constructed /// enum values. /// [Fact] public void PkVsPk_Collides_PkVsNonPk_staysExempt_ThroughRealTableLookup() { var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = PkGuid, PublicWeenieBitfield = 0x8u | 0x20u, // BF_PLAYER | BF_PLAYER_KILLER }); objects.AddOrUpdate(new ClientObject { ObjectId = NonPkGuid, PublicWeenieBitfield = 0x8u, // BF_PLAYER only — no PK status }); ObjectInfoState pkMoverState = ObjectInfoState.IsPlayer | EntityCollisionFlagsExt.ResolveMoverPvpState(objects, PkGuid); EntityCollisionFlags nonPkTargetFlags = EntityCollisionFlagsExt.FromPwdBitfield( objects.Get(NonPkGuid)!.PublicWeenieBitfield!.Value); EntityCollisionFlags pkTargetFlags = EntityCollisionFlagsExt.FromPwdBitfield( objects.Get(PkGuid)!.PublicWeenieBitfield!.Value); // PK mover vs a non-PK player target: retail exempts (walk through). Assert.True(CollisionExemption.ShouldSkip( targetState: 0u, targetFlags: nonPkTargetFlags, moverState: pkMoverState)); // PK mover vs a PK player target: retail collides. Assert.False(CollisionExemption.ShouldSkip( targetState: 0u, targetFlags: pkTargetFlags, moverState: pkMoverState)); // Non-PK mover vs non-PK target (the invariant): retail exempts, // matching the pre-P3 hardcoded IsPlayer-only behavior exactly. ObjectInfoState nonPkMoverState = ObjectInfoState.IsPlayer | EntityCollisionFlagsExt.ResolveMoverPvpState(objects, NonPkGuid); Assert.True(CollisionExemption.ShouldSkip( targetState: 0u, targetFlags: nonPkTargetFlags, moverState: nonPkMoverState)); } }