using AcDream.Core.Items; using Xunit; namespace AcDream.Core.Tests.Items; public sealed class ClientObjectTableTests { private static ClientObject MakeItem(uint id, string name = "Widget") => new ClientObject { ObjectId = id, WeenieClassId = 1, Name = name, Type = ItemType.Misc, StackSize = 1, Burden = 10, Value = 5, }; [Fact] public void AddOrUpdate_FiresAddedEvent() { var repo = new ClientObjectTable(); ClientObject? added = null; repo.ObjectAdded += i => added = i; var item = MakeItem(100); repo.AddOrUpdate(item); Assert.Same(item, added); Assert.Equal(1, repo.ObjectCount); Assert.Same(item, repo.Get(100)); } [Fact] public void AddOrUpdate_ExistingItem_FiresPropertiesUpdated() { var repo = new ClientObjectTable(); var item = MakeItem(100); repo.AddOrUpdate(item); int propUpdateCount = 0; repo.ObjectUpdated += _ => propUpdateCount++; repo.AddOrUpdate(item); // second call is an update Assert.Equal(1, propUpdateCount); } [Fact] public void MoveItem_UpdatesContainerAndFiresEvent() { var repo = new ClientObjectTable(); var item = MakeItem(100); repo.AddOrUpdate(item); uint seenOld = 999, seenNew = 999; repo.ObjectMoved += (it, oldC, newC) => { seenOld = oldC; seenNew = newC; }; repo.MoveItem(100, 42, newSlot: 3); Assert.Equal(0u, seenOld); // was not in any container initially Assert.Equal(42u, seenNew); Assert.Equal(42u, item.ContainerId); Assert.Equal(3, item.ContainerSlot); } [Fact] public void MoveItem_Nonexistent_ReturnsFalse() { var repo = new ClientObjectTable(); Assert.False(repo.MoveItem(999, 42)); } [Fact] public void Remove_FiresEventAndRemoves() { var repo = new ClientObjectTable(); var item = MakeItem(100); repo.AddOrUpdate(item); ClientObject? removed = null; repo.ObjectRemoved += i => removed = i; Assert.True(repo.Remove(100)); Assert.Same(item, removed); Assert.Null(repo.Get(100)); } [Fact] public void UpdateProperties_MergesIncomingBundle() { var repo = new ClientObjectTable(); var item = MakeItem(100); item.Properties.Ints[1] = 10; repo.AddOrUpdate(item); var patch = new PropertyBundle(); patch.Ints[2] = 20; // new patch.Ints[1] = 15; // overrides patch.Strings[100] = "desc"; repo.UpdateProperties(100, patch); Assert.Equal(15, item.Properties.Ints[1]); Assert.Equal(20, item.Properties.Ints[2]); Assert.Equal("desc", item.Properties.Strings[100]); } [Fact] public void Clear_RemovesAllItems() { var repo = new ClientObjectTable(); repo.AddOrUpdate(MakeItem(1)); repo.AddOrUpdate(MakeItem(2)); repo.AddOrUpdate(MakeItem(3)); repo.Clear(); Assert.Equal(0, repo.ObjectCount); } [Fact] public void UpdateIntProperty_uiEffects_setsEffectsAndFires() { var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x500000ABu }); ClientObject? fired = null; repo.ObjectUpdated += i => fired = i; bool ok = repo.UpdateIntProperty(0x500000ABu, ClientObjectTable.UiEffectsPropertyId, value: 0x9); Assert.True(ok); Assert.Equal(0x9u, repo.Get(0x500000ABu)!.Effects); Assert.Equal(0x9, repo.Get(0x500000ABu)!.Properties.Ints[ClientObjectTable.UiEffectsPropertyId]); Assert.NotNull(fired); } [Fact] public void UpdateIntProperty_unknownItem_returnsFalse() { var repo = new ClientObjectTable(); Assert.False(repo.UpdateIntProperty(0xDEADBEEFu, 18u, 1)); } [Fact] public void UpdateIntProperty_uiEffectsClearedToZero_clearsEffects() { // The core "item with mana vs out of mana" promise: a draining item whose // UiEffects clears to 0 must return to its base (un-tinted) icon. Guards // against a future `if (value != 0)` regression on the unconditional assign. var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x500000ACu, Effects = 0x1u }); repo.UpdateIntProperty(0x500000ACu, ClientObjectTable.UiEffectsPropertyId, value: 0x1); Assert.Equal(0x1u, repo.Get(0x500000ACu)!.Effects); repo.UpdateIntProperty(0x500000ACu, ClientObjectTable.UiEffectsPropertyId, value: 0); Assert.Equal(0u, repo.Get(0x500000ACu)!.Effects); } [Fact] public void ClientObject_NewFields_DefaultAndSettable() { var o = new ClientObject { ObjectId = 1, WielderId = 0x42u, ItemsCapacity = 24, ContainersCapacity = 7, Priority = 8u, Structure = 5, MaxStructure = 10, Workmanship = 7.5f, }; o.WeenieClassId = 0xABCDu; // now settable Assert.Equal(0x42u, o.WielderId); Assert.Equal(24, o.ItemsCapacity); Assert.Equal(7, o.ContainersCapacity); Assert.Equal(8u, o.Priority); Assert.Equal(5, o.Structure); Assert.Equal(10, o.MaxStructure); Assert.Equal(7.5f, o.Workmanship); Assert.Equal(0xABCDu, o.WeenieClassId); } [Fact] public void WeenieData_Construct() { var d = new WeenieData(Guid: 1, Name: "x", Type: ItemType.Misc, WeenieClassId: 2, IconId: 0x06001234u, IconOverlayId: 0, IconUnderlayId: 0, Effects: 0, Value: 5, StackSize: 1, StackSizeMax: 1, Burden: 10, ContainerId: 0x99u, WielderId: null, ValidLocations: null, CurrentWieldedLocation: null, Priority: null, ItemsCapacity: null, ContainersCapacity: null, Structure: null, MaxStructure: null, Workmanship: null); Assert.Equal(0x99u, d.ContainerId); } private static WeenieData FullWeenie(uint guid, uint icon = 0x06001234u, string name = "Sword", ItemType type = ItemType.MeleeWeapon, uint effects = 0, int? value = 100, int? stack = 1, uint? container = null, uint wcid = 0xABCDu) => new WeenieData(guid, name, type, wcid, icon, 0, 0, effects, value, stack, StackSizeMax: 1, Burden: 10, ContainerId: container, WielderId: null, ValidLocations: null, CurrentWieldedLocation: null, Priority: null, ItemsCapacity: null, ContainersCapacity: null, Structure: null, MaxStructure: null, Workmanship: null); [Fact] public void Ingest_NewItemWithNoPriorStub_Creates_AndFiresAdded() // the Coldeve bug { var table = new ClientObjectTable(); ClientObject? added = null; table.ObjectAdded += o => added = o; var obj = table.Ingest(FullWeenie(0x500000B0u)); Assert.NotNull(added); Assert.Equal(0x06001234u, table.Get(0x500000B0u)!.IconId); Assert.Equal(0xABCDu, obj.WeenieClassId); } [Fact] public void Ingest_Existing_PatchesInPlace_PreservesPropertyBundle() { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x500000B1u)); table.Get(0x500000B1u)!.Properties.Ints[999u] = 7; // simulate appraise ClientObject? updated = null; table.ObjectUpdated += o => updated = o; table.Ingest(FullWeenie(0x500000B1u, name: "Renamed")); Assert.NotNull(updated); Assert.Equal("Renamed", table.Get(0x500000B1u)!.Name); Assert.Equal(7, table.Get(0x500000B1u)!.Properties.Ints[999u]); // NOT clobbered } [Fact] public void Ingest_AbsentNullableField_DoesNotClobber() { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x500000B2u, value: 100)); var noValue = FullWeenie(0x500000B2u) with { Value = null }; table.Ingest(noValue); Assert.Equal(100, table.Get(0x500000B2u)!.Value); } [Fact] public void Ingest_Effects_AssignedUnconditionally_ClearsToZero() // D.5.2 contract { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x500000B3u, effects: 0x1u)); Assert.Equal(0x1u, table.Get(0x500000B3u)!.Effects); table.Ingest(FullWeenie(0x500000B3u, effects: 0u)); Assert.Equal(0u, table.Get(0x500000B3u)!.Effects); } [Fact] public void RecordMembership_CreatesEntry_AndSetsEquip() { var table = new ClientObjectTable(); table.RecordMembership(0x500000B4u, equip: EquipMask.MeleeWeapon); var o = table.Get(0x500000B4u); Assert.NotNull(o); Assert.Equal(EquipMask.MeleeWeapon, o!.CurrentlyEquippedLocation); Assert.Equal(0u, o.IconId); // data not set — CreateObject fills it } [Fact] public void Ingest_AfterMembership_FillsData_NoDuplicate() // out-of-order: PD then CreateObject { var table = new ClientObjectTable(); table.RecordMembership(0x500000B5u); table.Ingest(FullWeenie(0x500000B5u)); Assert.Equal(1, table.ObjectCount); Assert.Equal(0x06001234u, table.Get(0x500000B5u)!.IconId); } [Fact] public void Membership_AfterIngest_NoDuplicate_PreservesData() // out-of-order: CreateObject then PD { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x500000B6u)); // CreateObject first (ground/vendor item) table.RecordMembership(0x500000B6u, equip: EquipMask.MeleeWeapon); // then PD manifest Assert.Equal(1, table.ObjectCount); Assert.Equal(0x06001234u, table.Get(0x500000B6u)!.IconId); // data NOT clobbered by membership Assert.Equal(EquipMask.MeleeWeapon, table.Get(0x500000B6u)!.CurrentlyEquippedLocation); } [Fact] public void ContainerIndex_IngestThenContents_OrderedBySlot() { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x510u, container: 0xC0u)); table.Ingest(FullWeenie(0x511u, container: 0xC0u)); table.MoveItem(0x510u, 0xC0u, newSlot: 1); table.MoveItem(0x511u, 0xC0u, newSlot: 0); Assert.Equal(new[] { 0x511u, 0x510u }, table.GetContents(0xC0u)); } [Fact] public void ContainerIndex_Move_ReparentsBetweenContainers() { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x520u, container: 0xC1u)); table.MoveItem(0x520u, 0xC2u, newSlot: 0); Assert.Empty(table.GetContents(0xC1u)); Assert.Equal(new[] { 0x520u }, table.GetContents(0xC2u)); } [Fact] public void ContainerIndex_Remove_DropsFromContents() { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x530u, container: 0xC3u)); table.Remove(0x530u); Assert.Empty(table.GetContents(0xC3u)); } [Fact] public void GetContents_UnknownContainer_Empty() { var table = new ClientObjectTable(); Assert.Empty(table.GetContents(0xDEADu)); } [Fact] public void ContainerIndex_SlotChange_ResortsInPlace() // guards the Reindex same-container early-out { var table = new ClientObjectTable(); table.Ingest(FullWeenie(0x540u, container: 0xC4u)); table.Ingest(FullWeenie(0x541u, container: 0xC4u)); table.MoveItem(0x540u, 0xC4u, newSlot: 0); table.MoveItem(0x541u, 0xC4u, newSlot: 1); Assert.Equal(new[] { 0x540u, 0x541u }, table.GetContents(0xC4u)); // move 0x540 to a later slot WITHIN THE SAME container — order must flip table.MoveItem(0x540u, 0xC4u, newSlot: 5); Assert.Equal(new[] { 0x541u, 0x540u }, table.GetContents(0xC4u)); Assert.Equal(2, table.GetContents(0xC4u).Count); // no duplicate from the same-container move } }