using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime.Entities; namespace AcDream.Runtime.Tests.Entities; /// /// #297 review round 2 (preferred fix): /// keeps RuntimeEntityRecord.Snapshot.ObjectDescriptionFlags live /// against , so every /// downstream reader of the canonical snapshot (ObjDesc/appearance rebuild, /// placement/teleport mover-flags resolution, the vivid target indicator) /// inherits the live PK/PKLite/Free state instead of the value frozen at /// CreateObject. /// public sealed class RuntimeEntityPvpBitfieldSnapshotSyncTests { private const uint Guid = 0x70000020u; private static WorldSession.EntitySpawn Spawn(uint? objectDescriptionFlags) => new( Guid, new CreateObject.ServerPosition(0x0101FFFFu, 10f, 10f, 5f, 1f, 0f, 0f, 0f), 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "fixture", (uint)ItemType.Creature, null, 0x09000001u, ObjectDescriptionFlags: objectDescriptionFlags); [Fact] public void ObjectUpdated_WithLiveBitfield_RewritesSnapshotObjectDescriptionFlags() { var entities = new RuntimeEntityDirectory(); var objects = new ClientObjectTable(); using var sync = new RuntimeEntityPvpBitfieldSnapshotSync(entities, objects); WorldSession.EntitySpawn spawn = Spawn(0x8u); // BF_PLAYER only entities.AcceptCreate(spawn); // seeds InboundPhysicsStateController's own retained copy RuntimeEntityRecord record = entities.AddActive(spawn); objects.AddOrUpdate(new ClientObject { ObjectId = Guid, PublicWeenieBitfield = 0x8u, }); objects.UpdateIntProperty( Guid, ClientObjectTable.PlayerKillerStatusPropertyId, value: PlayerKillerStatusBitfield.PkLite); Assert.Equal(0x2000008u, record.Snapshot.ObjectDescriptionFlags); } [Fact] public void ObjectUpdated_UnrelatedProperty_DoesNotRewriteSnapshot() { var entities = new RuntimeEntityDirectory(); var objects = new ClientObjectTable(); using var sync = new RuntimeEntityPvpBitfieldSnapshotSync(entities, objects); WorldSession.EntitySpawn spawn = Spawn(0x8u); entities.AcceptCreate(spawn); RuntimeEntityRecord record = entities.AddActive(spawn); objects.AddOrUpdate(new ClientObject { ObjectId = Guid, PublicWeenieBitfield = 0x8u, }); // Effects (18) is unrelated to PK status; PublicWeenieBitfield is // untouched, so the idempotency guard must skip the rewrite. objects.UpdateIntProperty(Guid, ClientObjectTable.UiEffectsPropertyId, value: 4); Assert.Equal(0x8u, record.Snapshot.ObjectDescriptionFlags); } [Fact] public void ObjectUpdated_NoPublicWeenieBitfieldYet_NoOp() { var entities = new RuntimeEntityDirectory(); var objects = new ClientObjectTable(); using var sync = new RuntimeEntityPvpBitfieldSnapshotSync(entities, objects); RuntimeEntityRecord record = entities.AddActive(Spawn(null)); objects.AddOrUpdate(new ClientObject { ObjectId = Guid }); // No PublicWeenieBitfield yet (CreateObject omitted it) — the // handler must no-op rather than throw or write a bogus 0. objects.UpdateIntProperty(Guid, ClientObjectTable.UiEffectsPropertyId, value: 1); Assert.Null(record.Snapshot.ObjectDescriptionFlags); } [Fact] public void ObjectUpdated_NoMatchingActiveEntity_NoOp() { var entities = new RuntimeEntityDirectory(); var objects = new ClientObjectTable(); using var sync = new RuntimeEntityPvpBitfieldSnapshotSync(entities, objects); // Item exists in the object table but has no active Runtime entity // (e.g. an inventory item) — must not throw. objects.AddOrUpdate(new ClientObject { ObjectId = 0x80000001u, PublicWeenieBitfield = 0x8u, }); objects.UpdateIntProperty( 0x80000001u, ClientObjectTable.PlayerKillerStatusPropertyId, value: PlayerKillerStatusBitfield.Pk); Assert.Equal(0, entities.Count); } [Fact] public void ObjDescAfterPkUpdate_MergedSnapshotKeepsLiveBitfield() { // #297 F1: InboundPhysicsStateController.ApplyAcceptedObjDesc merges // a new ObjDesc onto _snapshots[guid] — a DIFFERENT store than the // active RuntimeEntityRecord.Snapshot this class also rewrites. // Without RuntimeEntityDirectory.TryRefreshObjectDescriptionFlags // keeping BOTH stores in lockstep, this ObjDesc would silently // revert the live PK-status bits back to whatever CreateObject // originally carried. var entities = new RuntimeEntityDirectory(); var objects = new ClientObjectTable(); using var sync = new RuntimeEntityPvpBitfieldSnapshotSync(entities, objects); WorldSession.EntitySpawn spawn = Spawn(0x8u); entities.AcceptCreate(spawn); entities.AddActive(spawn); objects.AddOrUpdate(new ClientObject { ObjectId = Guid, PublicWeenieBitfield = 0x8u, }); objects.UpdateIntProperty( Guid, ClientObjectTable.PlayerKillerStatusPropertyId, value: PlayerKillerStatusBitfield.PkLite); var update = new ObjDescEvent.Parsed( Guid, new CreateObject.ModelData( 0x04000001u, Array.Empty(), Array.Empty(), Array.Empty()), InstanceSequence: 0, ObjDescSequence: 1); Assert.True(entities.TryApplyObjDesc(update, out WorldSession.EntitySpawn accepted)); Assert.Equal(0x2000008u, accepted.ObjectDescriptionFlags); } [Fact] public void Dispose_UnsubscribesFromObjectUpdated() { var entities = new RuntimeEntityDirectory(); var objects = new ClientObjectTable(); var sync = new RuntimeEntityPvpBitfieldSnapshotSync(entities, objects); WorldSession.EntitySpawn spawn = Spawn(0x8u); entities.AcceptCreate(spawn); RuntimeEntityRecord record = entities.AddActive(spawn); objects.AddOrUpdate(new ClientObject { ObjectId = Guid, PublicWeenieBitfield = 0x8u, }); sync.Dispose(); objects.UpdateIntProperty( Guid, ClientObjectTable.PlayerKillerStatusPropertyId, value: PlayerKillerStatusBitfield.PkLite); Assert.Equal(0x8u, record.Snapshot.ObjectDescriptionFlags); } }