using AcDream.App.World; using AcDream.Core.Items; using AcDream.Core.Physics; namespace AcDream.App.Physics; /// /// #297 second edge: keeps a live entity's collision-shadow /// synced with its /// after a live /// PropertyInt(PlayerKillerStatus) update. /// /// /// The MOVER side of the retail PvP exemption already refreshes for free: /// reads /// straight from the table on /// every call, so once ClientObjectTable.UpdateIntProperty rewrites /// that field (see ) the next mover /// query already sees it. The TARGET side is different: CollisionExemption /// .ShouldSkip reads a decoded value /// cached on the entry at registration /// time (LiveEntityCollisionBuilder.Build). /// /// /// /// This class is the IMMEDIATE fix — it makes the shadow-registry flags /// track a PK-status change the moment PropertyInt 134 arrives, with no wait /// for any other event. It is NOT sufficient by itself: review round 2 (F1) /// found that the same LiveEntityCollisionBuilder.Build registration /// path re-runs on every ObjDesc/appearance change (any equip/dequip) and /// rebuilds the shadow flags from spawn.ObjectDescriptionFlags — the /// canonical Runtime snapshot, NOT this class's write target — so without a /// companion fix a later equip would silently revert the flags this class /// just wrote. /// closes that gap by keeping the snapshot itself live, so every rebuild /// (and the placement/teleport mover-flags path, which reads the snapshot /// directly) reproduces the same correct value instead of a stale one. The /// two classes write to two different stores (this one: the shadow /// registry's cached per-cell flags; that one: the canonical wire snapshot) /// and are not a duplicate-authority pair — removing either reopens a /// distinct symptom. /// /// /// /// Subscribes to every rather /// than filtering to PropertyInt 134 alone: the recompute is a cheap /// dictionary lookup plus a bitmask merge /// (, which /// short-circuits on an unchanged result — see its own remarks for why that /// guard is load-bearing), it is a no-op for any object without a live /// shadow registration, and it stays correct for any future /// PWD-bitfield-affecting property without another wiring change. /// /// internal sealed class LiveEntityPvpBitfieldSync : IDisposable { private readonly ClientObjectTable _objects; private readonly LiveEntityRuntime _liveEntities; private readonly ShadowObjectRegistry _shadows; private bool _disposed; public LiveEntityPvpBitfieldSync( ClientObjectTable objects, LiveEntityRuntime liveEntities, ShadowObjectRegistry shadows) { _objects = objects ?? throw new ArgumentNullException(nameof(objects)); _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); _objects.ObjectUpdated += OnObjectUpdated; } private void OnObjectUpdated(ClientObject item) { if (item.PublicWeenieBitfield is not { } bitfield) return; if (!_liveEntities.TryGetRecord(item.ObjectId, out LiveEntityRecord record) || record.WorldEntity is not { } entity) { return; } _shadows.UpdatePwdBitfieldFlags(entity.Id, bitfield); } public void Dispose() { if (_disposed) return; _disposed = true; _objects.ObjectUpdated -= OnObjectUpdated; } }