using AcDream.Core.Items; namespace AcDream.Core.Physics; /// /// Per-entity flags driving the retail-faithful PvP / Player / /// Impenetrable exemption logic in FindObjCollisions. Decoded /// from PublicWeenieDesc._bitfield at CreateObject time. /// /// /// Bit positions (verified against /// docs/research/named-retail/acclient_2013_pseudo_c.txt:406898-406918 /// where ACCWeenieObject::IsPK/IsPKLite/IsImpenetrable read directly /// from this->pwd._bitfield): /// /// /// BF_PLAYER = 0x8 (bit 3) → /// BF_PLAYER_KILLER = 0x20 (bit 5) → /// BF_FREE_PKSTATUS = 0x200000 (bit 21) → /// BF_PKLITE_PKSTATUS = 0x2000000 (bit 25) → /// /// /// /// is NOT a PWD bit — retail derives it from /// PublicWeenieDesc._type matching ITEM_TYPE_CREATURE /// (acclient.h ITEM_TYPE enum). Set at registration time by callers that /// already know the item type. /// /// [Flags] public enum EntityCollisionFlags : byte { None = 0x00, /// Set when BF_PLAYER (0x8) is set in pwd._bitfield. IsPlayer = 0x01, /// Derived from ItemType.Creature on the spawn payload. IsCreature = 0x02, /// Set when BF_PLAYER_KILLER (0x20) is set. IsPK = 0x04, /// Set when BF_PKLITE_PKSTATUS (0x2000000) is set. IsPKLite = 0x08, /// Set when BF_FREE_PKSTATUS (0x200000) is set (a.k.a. "Free" PK status — cannot be PKed). IsImpenetrable = 0x10, /// /// Runtime collision metadata: the target has a retail /// CWeenieObject. This is deliberately distinct from /// because doors, portals, and items also have a /// weenie. OBJECTINFO::missile_ignore at 0x0050CEB0 /// requires this distinction for ethereal targets. /// HasWeenie = 0x20, /// /// AP-71 (Campaign P Slice P4, 2026-07-30): set when BOTH /// BF_ADMIN (0x100000) and BF_IMMUNE_CELL_RESTRICTIONS /// (0x400000) are set in pwd._bitfield — the exact two-bit AND /// retail's ACCWeenieObject::CanBypassMoveRestrictions /// (0x0058c500) evaluates to let a mover through an access-restricted /// (house-barrier) cell without an owner/guest-list check. Bit names /// confirmed at acclient.h:6452-6454 /// (PublicWeenieDesc::BitfieldIndex); BF_ADMIN is /// independently decoded the same way in /// . /// CanBypassMoveRestrictions = 0x40, } /// Helpers to convert raw retail bitfields into . public static class EntityCollisionFlagsExt { /// /// #297 (target-side refresh): exactly the subset of /// that /// can produce — IsPlayer/IsPK/IsPKLite/IsImpenetrable/ /// CanBypassMoveRestrictions. Disjoint from /// (derived from ItemType) and /// (set once at registration), so a live PWD-bitfield refresh can replace /// exactly this mask without disturbing either. /// public const EntityCollisionFlags PwdBitfieldDerivedMask = EntityCollisionFlags.IsPlayer | EntityCollisionFlags.IsPK | EntityCollisionFlags.IsPKLite | EntityCollisionFlags.IsImpenetrable | EntityCollisionFlags.CanBypassMoveRestrictions; /// /// Decode the player/PK/PKLite/Impenetrable bits from a /// PublicWeenieDesc._bitfield value (the WeenieHeader trailer /// field acdream's parser surfaces as ObjectDescriptionFlags). /// /// Bit positions per /// docs/research/named-retail/acclient.h:6431-6463 /// (PublicWeenieDesc::BitfieldIndex) and /// acclient_2013_pseudo_c.txt:441868-441890 /// (PublicWeenieDesc::SetPlayerKillerStatus). /// public static EntityCollisionFlags FromPwdBitfield(uint bitfield) { var flags = EntityCollisionFlags.None; if ((bitfield & 0x8u) != 0) flags |= EntityCollisionFlags.IsPlayer; if ((bitfield & 0x20u) != 0) flags |= EntityCollisionFlags.IsPK; if ((bitfield & 0x200000u) != 0) flags |= EntityCollisionFlags.IsImpenetrable; if ((bitfield & 0x2000000u) != 0) flags |= EntityCollisionFlags.IsPKLite; // AP-71: BF_ADMIN (0x100000) AND BF_IMMUNE_CELL_RESTRICTIONS (0x400000), // matching CanBypassMoveRestrictions' own AND (not OR) of the two bits. if ((bitfield & 0x100000u) != 0 && (bitfield & 0x400000u) != 0) flags |= EntityCollisionFlags.CanBypassMoveRestrictions; return flags; } /// /// TS-23 (Campaign P Slice P3, 2026-07-30): translate the decoded /// per-entity (the PWD-bitfield /// bit-space) into the bits /// CollisionExemption.ShouldSkip and the moverFlags argument to /// PhysicsEngine.ResolveWithTransition actually consume — a /// DIFFERENT bit-space (retail OBJECTINFO::init 0x0050cf30 /// `state |= 0x80/0x800/0x1000`, acclient.h:6190-6194) that must not be /// confused with the PWD wire numbering. /// is deliberately NOT translated here — every existing mover-flags call /// site already derives from its /// own GUID-prefix heuristic (correct per #184 Slice 2b) and this helper /// only fills the gap that heuristic cannot: PK/PKLite/Impenetrable. /// public static ObjectInfoState ToMoverState(this EntityCollisionFlags flags) { var state = ObjectInfoState.None; if ((flags & EntityCollisionFlags.IsPK) != 0) state |= ObjectInfoState.IsPK; if ((flags & EntityCollisionFlags.IsPKLite) != 0) state |= ObjectInfoState.IsPKLite; if ((flags & EntityCollisionFlags.IsImpenetrable) != 0) state |= ObjectInfoState.IsImpenetrable; // AP-71 (Campaign P Slice P4): the mover's own house-restriction bypass, // consumed by ObjectInfo.CheckEntryRestrictions. if ((flags & EntityCollisionFlags.CanBypassMoveRestrictions) != 0) state |= ObjectInfoState.CanBypassMoveRestrictions; return state; } /// /// TS-23 (Campaign P Slice P3, 2026-07-30): the one shared /// ClientObjectTable-backed mover-flags lookup every physics /// call site (local player world-entry, remote DR sweep + teleport, /// ordinary movers) uses — was inlined three times (App /// GameWindow/LivePresentationComposition/ /// RemoteTeleportController) before being consolidated here. /// An entity with no row, or a row with no wire bitfield yet, resolves /// to — a no-op OR into moverFlags, /// bit-identical to every pre-P3 caller. /// public static ObjectInfoState ResolveMoverPvpState(this ClientObjectTable objects, uint serverGuid) { ArgumentNullException.ThrowIfNull(objects); return objects.Get(serverGuid)?.PublicWeenieBitfield is { } bitfield ? FromPwdBitfield(bitfield).ToMoverState() : ObjectInfoState.None; } }