using AcDream.App.UI; using AcDream.Core.Items; namespace AcDream.App.Tests.UI; public sealed class ItemInteractionControllerTests { private const uint Player = 0x50000001u; private const uint Pack = 0x50000010u; // USEABLE_SOURCE_CONTAINED_TARGET_REMOTE_OR_SELF (acclient.h ITEM_USEABLE; // ACE Usable.SourceContainedTargetRemoteOrSelf) — the classic healing-kit value. private const uint HealthKitUseability = 0x00220008u; private sealed class Harness { public readonly ClientObjectTable Objects = new(); public readonly List Uses = new(); public readonly List Examines = new(); public readonly List<(uint Source, uint Target)> UseWithTarget = new(); public readonly List<(uint Item, uint Mask)> Wields = new(); public readonly List<(uint Item, uint Container, int Placement)> Puts = new(); public readonly List<(uint Item, uint Container, uint Placement, uint Amount)> SplitPuts = new(); public readonly List ExternalRequests = new(); public readonly List Drops = new(); public readonly List<(uint Item, uint Amount)> SplitDrops = new(); public readonly List<(uint Target, uint Item, uint Amount)> Gives = new(); public readonly List Toasts = new(); public readonly List SystemMessages = new(); public readonly StackSplitQuantityState SplitQuantity = new(); public uint SelectedObject; public bool NonCombatMode; public bool DragOnPlayerOpensSecureTrade = true; public uint GroundObject; public long Now = 1_000; public Harness() { Objects.AddOrUpdate(new ClientObject { ObjectId = Player, Name = "Player", Type = ItemType.Creature, }); Objects.AddOrUpdate(new ClientObject { ObjectId = Pack, Name = "Backpack", Type = ItemType.Container, ItemsCapacity = 24, }); Objects.MoveItem(Pack, Player, 0); Controller = new ItemInteractionController( Objects, playerGuid: () => Player, sendUse: Uses.Add, sendExamine: Examines.Add, sendUseWithTarget: (source, target) => UseWithTarget.Add((source, target)), sendWield: (item, mask) => Wields.Add((item, mask)), sendDrop: Drops.Add, nowMs: () => Now, toast: Toasts.Add, sendSplitToWorld: (item, amount) => SplitDrops.Add((item, amount)), selectedObjectId: () => SelectedObject, stackSplitQuantity: SplitQuantity, inNonCombatMode: () => NonCombatMode, groundObjectId: () => GroundObject, sendPutItemInContainer: (item, container, placement) => Puts.Add((item, container, placement)), sendGive: (target, item, amount) => Gives.Add((target, item, amount)), dragOnPlayerOpensSecureTrade: () => DragOnPlayerOpensSecureTrade, systemMessage: SystemMessages.Add, sendSplitToContainer: (item, container, placement, amount) => SplitPuts.Add((item, container, placement, amount)), requestExternalContainer: id => { GroundObject = id; ExternalRequests.Add(id); }); } public ItemInteractionController Controller { get; } public ClientObject AddContained(uint id, Action? configure = null) { var item = new ClientObject { ObjectId = id, Name = $"Item {id:X}", Type = ItemType.Misc, IconId = 0x06001234u, }; configure?.Invoke(item); Objects.AddOrUpdate(item); Objects.MoveItem(id, Pack, Objects.GetContents(Pack).Count); return item; } } [Fact] public void TargetedItem_entersTargetModeAndMarksPendingSource() { var h = new Harness(); h.AddContained(0x50000A01u, item => item.Useability = HealthKitUseability); Assert.True(h.Controller.ActivateItem(0x50000A01u)); Assert.True(h.Controller.IsTargetModeActive); Assert.True(h.Controller.IsPendingSource(0x50000A01u)); Assert.Equal(InteractionModeKind.UseItemOnTarget, h.Controller.InteractionState.Current.Kind); Assert.Equal(0x50000A01u, h.Controller.InteractionState.Current.SourceObjectId); Assert.Empty(h.UseWithTarget); } [Fact] public void ExternalContainerUse_RequestsGroundObjectAndSendsUse() { var h = new Harness(); const uint chest = 0x70000001u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = chest, Type = ItemType.Container, ItemsCapacity = 24, Useability = ItemUseability.Remote, }); Assert.True(h.Controller.ActivateItem(chest)); Assert.Equal(new uint[] { chest }, h.Uses); Assert.Equal(new uint[] { chest }, h.ExternalRequests); Assert.Equal(chest, h.GroundObject); } [Fact] public void CorpseUse_UsesStuckContainerInsteadOfTryingToPickItUp() { var h = new Harness(); const uint corpse = 0x70000002u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = corpse, Type = ItemType.Container, ItemsCapacity = 24, Useability = ItemUseability.Remote, PublicWeenieBitfield = (uint)( PublicWeenieFlags.Openable | PublicWeenieFlags.Stuck | PublicWeenieFlags.Corpse), }); Assert.True(h.Controller.UseSelectedOrEnterMode(corpse)); Assert.Equal(new uint[] { corpse }, h.Uses); Assert.Equal(new uint[] { corpse }, h.ExternalRequests); Assert.Empty(h.Puts); } [Fact] public void DropOwnedItemOnOpenExternalContainer_SendsPutWithoutOptimisticMove() { var h = new Harness(); const uint itemId = 0x50000A33u; const uint chest = 0x70000001u; ClientObject item = h.AddContained(itemId); h.Objects.AddOrUpdate(new ClientObject { ObjectId = chest, Type = ItemType.Container, ItemsCapacity = 24, PublicWeenieBitfield = (uint)PublicWeenieFlags.Openable, }); h.GroundObject = chest; var cell = new UiItemSlot(); cell.SetItem(itemId, 0u); var payload = new ItemDragPayload(itemId, ItemDragSource.Inventory, 0, cell); Assert.True(h.Controller.PlaceIn3D(payload, chest)); Assert.Equal(new[] { (itemId, chest, 0) }, h.Puts); Assert.Equal(Pack, item.ContainerId); } [Fact] public void PrimaryClickRouter_distinguishesInactiveSuccessAndConsumedRejection() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = HealthKitUseability; item.TargetType = (uint)ItemType.Creature; }); h.AddContained(0x50000A02u, item => item.Type = ItemType.Armor); Assert.Equal(ItemPrimaryClickResult.NotActive, h.Controller.OfferPrimaryClick(Player)); h.Controller.ActivateItem(0x50000A01u); Assert.Equal(ItemPrimaryClickResult.ConsumedRejected, h.Controller.OfferPrimaryClick(0x50000A02u)); Assert.False(h.Controller.IsTargetModeActive); h.Now += 200; h.Controller.ActivateItem(0x50000A01u); Assert.Equal(ItemPrimaryClickResult.ConsumedSuccess, h.Controller.OfferSelfPrimaryClick()); Assert.Equal(new[] { (0x50000A01u, Player) }, h.UseWithTarget); } [Fact] public void ToolbarUse_withoutSelection_armsOneShotUseMode() { var h = new Harness(); h.AddContained(0x50000A10u, item => item.Useability = ItemUseability.Contained); Assert.True(h.Controller.UseSelectedOrEnterMode(0u)); Assert.True(h.Controller.IsAnyTargetModeActive); Assert.Equal(InteractionModeKind.Use, h.Controller.InteractionState.Current.Kind); Assert.Equal(ItemPrimaryClickResult.ConsumedSuccess, h.Controller.OfferPrimaryClick(0x50000A10u)); Assert.Equal(new[] { 0x50000A10u }, h.Uses); Assert.False(h.Controller.IsAnyTargetModeActive); } [Fact] public void ToolbarExamine_usesSelectionOrArmsOneShotExamineMode() { var h = new Harness(); Assert.True(h.Controller.ExamineSelectedOrEnterMode(Player)); Assert.Equal(new[] { Player }, h.Examines); Assert.True(h.Controller.ExamineSelectedOrEnterMode(0u)); Assert.Equal(InteractionModeKind.Examine, h.Controller.InteractionState.Current.Kind); Assert.Equal(ItemPrimaryClickResult.ConsumedSuccess, h.Controller.OfferPrimaryClick(Pack)); Assert.Equal(new[] { Player, Pack }, h.Examines); Assert.False(h.Controller.IsAnyTargetModeActive); } [Fact] public void SelfTarget_sendsUseWithTargetAndClearsTargetMode() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = HealthKitUseability; item.TargetType = (uint)ItemType.Creature; // kits target creatures/players }); h.Controller.ActivateItem(0x50000A01u); Assert.True(h.Controller.AcquireSelfTarget()); Assert.Equal(new[] { (0x50000A01u, Player) }, h.UseWithTarget); Assert.False(h.Controller.IsTargetModeActive); } [Fact] public void ActivateTargetItemDuringTargetMode_usesClickedItemAsTarget() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = 0x00080008u; // TARGET_CONTAINED tool item.TargetType = (uint)ItemType.Misc; // kind gate must pass for the send }); h.AddContained(0x50000A02u); h.Controller.ActivateItem(0x50000A01u); Assert.True(h.Controller.ActivateItem(0x50000A02u)); Assert.Equal(new[] { (0x50000A01u, 0x50000A02u) }, h.UseWithTarget); Assert.False(h.Controller.IsTargetModeActive); } // ── Retail target-compat gate (ItemHolder::IsTargetCompatibleWithTargetingObject 0x00588070) ── [Fact] public void TargetCompat_wrongKind_refusesAndClearsMode() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = HealthKitUseability; item.TargetType = (uint)ItemType.Creature; }); h.AddContained(0x50000A02u, item => item.Type = ItemType.Armor); // a coat h.Controller.ActivateItem(0x50000A01u); Assert.False(h.Controller.ActivateItem(0x50000A02u)); // kind gate: Creature mask & Armor = 0 Assert.Empty(h.UseWithTarget); Assert.False(h.Controller.IsTargetModeActive); } [Fact] public void IsCurrentTargetCompatible_appliesKindGateForCursor() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = HealthKitUseability; item.TargetType = (uint)ItemType.Creature; }); h.AddContained(0x50000A02u, item => item.Type = ItemType.Armor); h.Controller.ActivateItem(0x50000A01u); Assert.True(h.Controller.IsCurrentTargetCompatible(Player)); // self: Self bit + Creature kind Assert.False(h.Controller.IsCurrentTargetCompatible(0x50000A02u)); // coat: yellow-over-items bug pin } [Fact] public void SelfTarget_requiresSelfBit() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = 0x00200008u; // TARGET_REMOTE only — no Self bit item.TargetType = (uint)ItemType.Creature; }); h.Controller.ActivateItem(0x50000A01u); Assert.False(h.Controller.AcquireSelfTarget()); // IsUseable_SelfTarget 0x004fcd30 Assert.Empty(h.UseWithTarget); } [Fact] public void MissingTargetTypeMask_matchesNothing() { var h = new Harness(); h.AddContained(0x50000A01u, item => item.Useability = HealthKitUseability); // no TargetType on wire h.Controller.ActivateItem(0x50000A01u); Assert.False(h.Controller.AcquireSelfTarget()); // retail: 0 mask & anything == 0 Assert.Empty(h.UseWithTarget); } [Fact] public void ContainedTarget_refusesNonOwnedWorldObject() { var h = new Harness(); h.AddContained(0x50000A01u, item => { item.Useability = 0x00080008u; // Contained is the least-limited target bit item.TargetType = (uint)ItemType.Misc; }); // right KIND, but out in the world — Contained requires our inventory: h.Objects.AddOrUpdate(new ClientObject { ObjectId = 0x60000001u, Type = ItemType.Misc }); h.Controller.ActivateItem(0x50000A01u); Assert.False(h.Controller.AcquireTarget(0x60000001u)); Assert.Empty(h.UseWithTarget); } [Fact] public void DirectUseItem_sendsUse() { var h = new Harness(); h.AddContained(0x50000A03u, item => item.Useability = ItemUseability.Contained); Assert.True(h.Controller.ActivateItem(0x50000A03u)); Assert.Equal(new[] { 0x50000A03u }, h.Uses); } [Fact] public void ContainerItem_sendsUseToOpen() { var h = new Harness(); h.AddContained(0x50000A04u, item => { item.Type = ItemType.Container; item.ItemsCapacity = 12; }); Assert.True(h.Controller.ActivateItem(0x50000A04u)); Assert.Equal(new[] { 0x50000A04u }, h.Uses); } [Fact] public void EquippableItemWithFreeSlot_sendsGetAndWieldAndMovesOptimistically() { var h = new Harness(); h.AddContained(0x50000A05u, item => { item.Type = ItemType.Clothing; item.ValidLocations = EquipMask.HeadWear; }); Assert.True(h.Controller.ActivateItem(0x50000A05u)); Assert.Equal(new[] { (0x50000A05u, (uint)EquipMask.HeadWear) }, h.Wields); var equipped = h.Objects.Get(0x50000A05u)!; Assert.Equal(Player, equipped.ContainerId); Assert.Equal(EquipMask.HeadWear, equipped.CurrentlyEquippedLocation); } [Fact] public void EquippableMultiSlotItemWithFreeSlots_sendsFullCoverageMaskAndMovesOptimistically() { var h = new Harness(); const EquipMask coatMask = EquipMask.ChestWear | EquipMask.UpperArmWear | EquipMask.LowerArmWear; h.AddContained(0x50000A15u, item => { item.Type = ItemType.Clothing; item.ValidLocations = coatMask; }); Assert.True(h.Controller.ActivateItem(0x50000A15u)); Assert.Equal(new[] { (0x50000A15u, (uint)coatMask) }, h.Wields); var equipped = h.Objects.Get(0x50000A15u)!; Assert.Equal(Player, equipped.ContainerId); Assert.Equal(coatMask, equipped.CurrentlyEquippedLocation); } [Fact] public void AutoWearItemWithOverlappingSlotButDifferentPriority_sendsFullMask() { var h = new Harness(); h.Objects.AddOrUpdate(new ClientObject { ObjectId = 0x50000AF1u, Type = ItemType.Clothing, CurrentlyEquippedLocation = EquipMask.UpperArmWear, Priority = 0x00000001u, }); h.Objects.MoveItem(0x50000AF1u, Player, -1, EquipMask.UpperArmWear); const EquipMask coatMask = EquipMask.ChestWear | EquipMask.UpperArmWear | EquipMask.LowerArmWear; h.AddContained(0x50000A16u, item => { item.Type = ItemType.Clothing; item.ValidLocations = coatMask; item.Priority = 0x00000002u; }); Assert.True(h.Controller.ActivateItem(0x50000A16u)); Assert.Equal(new[] { (0x50000A16u, (uint)coatMask) }, h.Wields); Assert.Equal(coatMask, h.Objects.Get(0x50000A16u)!.CurrentlyEquippedLocation); } [Fact] public void AutoWearItemWithOverlappingSlotAndPriority_sendsNothing() { var h = new Harness(); h.Objects.AddOrUpdate(new ClientObject { ObjectId = 0x50000AF2u, Type = ItemType.Clothing, CurrentlyEquippedLocation = EquipMask.UpperArmWear, Priority = 0x00000004u, }); h.Objects.MoveItem(0x50000AF2u, Player, -1, EquipMask.UpperArmWear); const EquipMask coatMask = EquipMask.ChestWear | EquipMask.UpperArmWear | EquipMask.LowerArmWear; h.AddContained(0x50000A17u, item => { item.Type = ItemType.Clothing; item.ValidLocations = coatMask; item.Priority = 0x00000004u; }); Assert.False(h.Controller.ActivateItem(0x50000A17u)); Assert.Empty(h.Wields); Assert.Equal(Pack, h.Objects.Get(0x50000A17u)!.ContainerId); } [Fact] public void EquippableItemWithNoFreeSlot_sendsNothing() { var h = new Harness(); h.Objects.AddOrUpdate(new ClientObject { ObjectId = 0x50000AF0u, Type = ItemType.Armor, CurrentlyEquippedLocation = EquipMask.Shield, }); h.Objects.MoveItem(0x50000AF0u, Player, -1, EquipMask.Shield); h.AddContained(0x50000A06u, item => { item.Type = ItemType.Armor; item.ValidLocations = EquipMask.Shield; }); Assert.False(h.Controller.ActivateItem(0x50000A06u)); Assert.Empty(h.Wields); Assert.Equal(Pack, h.Objects.Get(0x50000A06u)!.ContainerId); } [Theory] [InlineData(true)] [InlineData(false)] public void WeaponReplacement_inPeaceOrWar_unwieldsThenWieldsAfterServerConfirm( bool nonCombatMode) { var h = new Harness { NonCombatMode = nonCombatMode }; const uint sword = 0x50000B01u; const uint bow = 0x50000B02u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = sword, Name = "Sword", Type = ItemType.MeleeWeapon, CombatUse = 1, ValidLocations = EquipMask.MeleeWeapon, }); h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon); h.AddContained(bow, item => { item.Name = "Bow"; item.Type = ItemType.MissileWeapon; item.CombatUse = 1; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.ActivateItem(bow)); Assert.Equal(new[] { (sword, Player, 0) }, h.Puts); Assert.Empty(h.Wields); Assert.Equal(EquipMask.MeleeWeapon, h.Objects.Get(sword)!.CurrentlyEquippedLocation); Assert.Equal(Pack, h.Objects.Get(bow)!.ContainerId); // Authoritative 0x0022: only now does retail retry AutoWield. h.Objects.MoveItem(sword, Player, 0, EquipMask.None); Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields); Assert.Equal(EquipMask.None, h.Objects.Get(sword)!.CurrentlyEquippedLocation); Assert.Equal(EquipMask.MissileWeapon, h.Objects.Get(bow)!.CurrentlyEquippedLocation); Assert.Equal(Player, h.Objects.Get(bow)!.ContainerId); } [Fact] public void PreviouslyWieldedBow_AfterAuthoritativeUnwield_DoubleClickWieldsAgain() { var h = new Harness(); const uint bow = 0x50000B03u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = bow, Name = "Bow", Type = ItemType.MissileWeapon, CombatUse = 1, ValidLocations = EquipMask.MissileWeapon, ContainerId = Player, WielderId = Player, CurrentlyEquippedLocation = EquipMask.MissileWeapon, }); // Retail ServerSaysMoveItem clears _wielderID when this bow leaves // the paper doll. This is the login-existing-bow regression: leaving // the old player id here makes DetermineUseResult say "already wielded." Assert.True(h.Objects.ApplyServerMove( bow, Pack, newWielderId: 0u, newSlot: 0)); Assert.True(h.Controller.ActivateItem(bow)); Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields); } [Fact] public void WeaponReplacement_doesNotOverlapRequestsWhileAwaitingServer() { var h = new Harness(); const uint sword = 0x50000B11u; const uint bow = 0x50000B12u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = sword, Type = ItemType.MeleeWeapon, CombatUse = 1, ValidLocations = EquipMask.MeleeWeapon, }); h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon); h.AddContained(bow, item => { item.Type = ItemType.MissileWeapon; item.CombatUse = 1; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.ActivateItem(bow)); h.Now += 200; Assert.False(h.Controller.ActivateItem(bow)); Assert.Single(h.Puts); Assert.Empty(h.Wields); } [Fact] public void WeaponReplacement_doesNotOverlapWhileAwaitingFinalWieldConfirmation() { var h = new Harness(); const uint sword = 0x50000B15u; const uint bow = 0x50000B16u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = sword, Type = ItemType.MeleeWeapon, CombatUse = 1, ValidLocations = EquipMask.MeleeWeapon, }); h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon); h.AddContained(bow, item => { item.Type = ItemType.MissileWeapon; item.CombatUse = 2; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.ActivateItem(bow)); h.Objects.MoveItem(sword, Player, 0, EquipMask.None); Assert.Single(h.Wields); // Bow is optimistic but ACE has not sent WieldObject yet. Retail's // waiting state consumes this attempted reverse switch. h.Now += 200; Assert.False(h.Controller.ActivateItem(sword)); Assert.Single(h.Puts); Assert.Single(h.Wields); // A separate reconciliation record for the same item must not delay // retail's exact IR_WIELD completion boundary. Assert.True(h.Objects.WieldItemOptimistic( bow, Player, EquipMask.MissileWeapon)); // Authoritative WieldObject completes AutoWield even // though the object table still has another rollback record. Assert.True(h.Objects.ApplyConfirmedServerWield( bow, Player, EquipMask.MissileWeapon)); h.Now += 200; Assert.True(h.Controller.ActivateItem(sword)); Assert.Equal( new[] { (sword, Player, 0), (bow, Player, 0) }, h.Puts); } [Fact] public void WeaponReplacement_serverReject_clearsPendingTransactionForRetry() { var h = new Harness(); const uint sword = 0x50000B21u; const uint bow = 0x50000B22u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = sword, Type = ItemType.MeleeWeapon, CombatUse = 1, ValidLocations = EquipMask.MeleeWeapon, }); h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon); h.AddContained(bow, item => { item.Type = ItemType.MissileWeapon; item.CombatUse = 1; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.ActivateItem(bow)); Assert.False(h.Objects.RejectMove(sword, 0x04FFu)); h.Now += 200; Assert.True(h.Controller.ActivateItem(bow)); Assert.Equal(2, h.Puts.Count); Assert.Empty(h.Wields); Assert.Equal(EquipMask.MeleeWeapon, h.Objects.Get(sword)!.CurrentlyEquippedLocation); } [Fact] public void WeaponReplacement_bow_removesPrimaryThenIncompatibleShield() { var h = new Harness(); const uint sword = 0x50000B31u; const uint shield = 0x50000B32u; const uint bow = 0x50000B33u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = sword, Type = ItemType.MeleeWeapon, CombatUse = 1, ValidLocations = EquipMask.MeleeWeapon, }); h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon); h.Objects.AddOrUpdate(new ClientObject { ObjectId = shield, Type = ItemType.Armor, CombatUse = 4, ValidLocations = EquipMask.Shield, }); h.Objects.MoveItem(shield, Player, -1, EquipMask.Shield); h.AddContained(bow, item => { item.Type = ItemType.MissileWeapon; item.CombatUse = 2; item.AmmoType = 1; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.ActivateItem(bow)); Assert.Equal(new[] { (sword, Player, 0) }, h.Puts); h.Objects.MoveItem(sword, Player, 0, EquipMask.None); Assert.Equal( new[] { (sword, Player, 0), (shield, Player, 0) }, h.Puts); Assert.Empty(h.Wields); h.Objects.MoveItem(shield, Player, 0, EquipMask.None); Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields); } [Fact] public void WeaponReplacement_bow_removesMismatchedAmmoAfterPrimaryBlocker() { var h = new Harness(); const uint sword = 0x50000B41u; const uint arrows = 0x50000B42u; const uint bow = 0x50000B43u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = sword, Type = ItemType.MeleeWeapon, CombatUse = 1, ValidLocations = EquipMask.MeleeWeapon, }); h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon); h.Objects.AddOrUpdate(new ClientObject { ObjectId = arrows, Type = ItemType.MissileWeapon, CombatUse = 3, AmmoType = 2, ValidLocations = EquipMask.MissileAmmo, }); h.Objects.MoveItem(arrows, Player, -1, EquipMask.MissileAmmo); h.AddContained(bow, item => { item.Type = ItemType.MissileWeapon; item.CombatUse = 2; item.AmmoType = 1; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.ActivateItem(bow)); h.Objects.MoveItem(sword, Player, 0, EquipMask.None); Assert.Equal( new[] { (sword, Player, 0), (arrows, Player, 0) }, h.Puts); h.Objects.MoveItem(arrows, Player, 0, EquipMask.None); Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields); } [Fact] public void PaperdollWeaponSwitch_toSword_keepsEquippedArrows() { var h = new Harness(); const uint bow = 0x50000B51u; const uint arrows = 0x50000B52u; const uint sword = 0x50000B53u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = bow, Name = "Shortbow", Type = ItemType.MissileWeapon, CombatUse = 2, AmmoType = 1, ValidLocations = EquipMask.MissileWeapon, }); h.Objects.MoveItem(bow, Player, -1, EquipMask.MissileWeapon); h.Objects.AddOrUpdate(new ClientObject { ObjectId = arrows, Name = "Arrows", Type = ItemType.MissileWeapon, CombatUse = 3, AmmoType = 1, ValidLocations = EquipMask.MissileAmmo, }); h.Objects.MoveItem(arrows, Player, -1, EquipMask.MissileAmmo); h.AddContained(sword, item => { item.Name = "Katar"; item.Type = ItemType.MeleeWeapon; item.CombatUse = 1; item.ValidLocations = EquipMask.MeleeWeapon; }); Assert.True(h.Controller.WieldFromPaperdoll(sword, EquipMask.MeleeWeapon)); Assert.Equal(new[] { (bow, Player, 0) }, h.Puts); Assert.Empty(h.Wields); Assert.Equal(new[] { "Moving Shortbow to your backpack" }, h.SystemMessages); h.Objects.MoveItem(bow, Player, 0, EquipMask.None); Assert.Equal(new[] { (sword, (uint)EquipMask.MeleeWeapon) }, h.Wields); Assert.Equal(EquipMask.MissileAmmo, h.Objects.Get(arrows)!.CurrentlyEquippedLocation); } [Fact] public void PaperdollWeaponSwitch_fromWandToSword_movesHeldCasterFirst() { var h = new Harness(); const uint wand = 0x50000B54u; const uint sword = 0x50000B55u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = wand, Name = "Wand", Type = ItemType.Caster, ValidLocations = EquipMask.Held, }); h.Objects.MoveItem(wand, Player, -1, EquipMask.Held); h.AddContained(sword, item => { item.Name = "Katar"; item.Type = ItemType.MeleeWeapon; item.CombatUse = 1; item.ValidLocations = EquipMask.MeleeWeapon; }); Assert.True(h.Controller.WieldFromPaperdoll(sword, EquipMask.MeleeWeapon)); Assert.Equal(new[] { (wand, Player, 0) }, h.Puts); Assert.Empty(h.Wields); Assert.Equal(new[] { "Moving Wand to your backpack" }, h.SystemMessages); h.Objects.MoveItem(wand, Player, 0, EquipMask.None); Assert.Equal(new[] { (sword, (uint)EquipMask.MeleeWeapon) }, h.Wields); } [Fact] public void PaperdollWeaponSwitch_toCrossbow_movesIncompatibleArrowsAfterBow() { var h = new Harness(); const uint bow = 0x50000B61u; const uint arrows = 0x50000B62u; const uint crossbow = 0x50000B63u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = bow, Name = "Shortbow", Type = ItemType.MissileWeapon, CombatUse = 2, AmmoType = 1, ValidLocations = EquipMask.MissileWeapon, }); h.Objects.MoveItem(bow, Player, -1, EquipMask.MissileWeapon); h.Objects.AddOrUpdate(new ClientObject { ObjectId = arrows, Name = "Arrows", Type = ItemType.MissileWeapon, CombatUse = 3, AmmoType = 1, ValidLocations = EquipMask.MissileAmmo, }); h.Objects.MoveItem(arrows, Player, -1, EquipMask.MissileAmmo); h.AddContained(crossbow, item => { item.Name = "Heavy Crossbow"; item.Type = ItemType.MissileWeapon; item.CombatUse = 2; item.AmmoType = 2; item.ValidLocations = EquipMask.MissileWeapon; }); Assert.True(h.Controller.WieldFromPaperdoll( crossbow, EquipMask.MissileWeapon)); Assert.Equal(new[] { (bow, Player, 0) }, h.Puts); h.Objects.MoveItem(bow, Player, 0, EquipMask.None); Assert.Equal( new[] { (bow, Player, 0), (arrows, Player, 0) }, h.Puts); Assert.Empty(h.Wields); h.Objects.MoveItem(arrows, Player, 0, EquipMask.None); Assert.Equal( new[] { (crossbow, (uint)EquipMask.MissileWeapon) }, h.Wields); Assert.Equal( new[] { "Moving Shortbow to your backpack", "Moving Arrows to your backpack", }, h.SystemMessages); } [Fact] public void PaperdollDrop_relocatesAlreadyEquippedItemToExplicitCompatibleSlot() { var h = new Harness(); const uint ring = 0x50000B71u; h.Objects.AddOrUpdate(new ClientObject { ObjectId = ring, Name = "Ring", Type = ItemType.Jewelry, ValidLocations = EquipMask.FingerWearLeft | EquipMask.FingerWearRight, }); h.Objects.MoveItem(ring, Player, -1, EquipMask.FingerWearLeft); Assert.True(h.Controller.WieldFromPaperdoll(ring, EquipMask.FingerWearRight)); Assert.Equal( new[] { (ring, (uint)EquipMask.FingerWearRight) }, h.Wields); Assert.Equal( EquipMask.FingerWearRight, h.Objects.Get(ring)!.CurrentlyEquippedLocation); } [Fact] public void PaperdollDrop_movesExplicitDestinationBlockerBeforeRelocatingEquippedItem() { var h = new Harness(); const uint leftRing = 0x50000B72u; const uint rightRing = 0x50000B73u; EquipMask valid = EquipMask.FingerWearLeft | EquipMask.FingerWearRight; h.Objects.AddOrUpdate(new ClientObject { ObjectId = leftRing, Name = "Left Ring", Type = ItemType.Jewelry, ValidLocations = valid, }); h.Objects.MoveItem(leftRing, Player, -1, EquipMask.FingerWearLeft); h.Objects.AddOrUpdate(new ClientObject { ObjectId = rightRing, Name = "Right Ring", Type = ItemType.Jewelry, ValidLocations = valid, }); h.Objects.MoveItem(rightRing, Player, -1, EquipMask.FingerWearRight); Assert.True(h.Controller.WieldFromPaperdoll(leftRing, EquipMask.FingerWearRight)); Assert.Equal(new[] { (rightRing, Player, 0) }, h.Puts); Assert.Empty(h.Wields); Assert.Equal(new[] { "Moving Right Ring to your backpack" }, h.SystemMessages); h.Objects.MoveItem(rightRing, Player, 0, EquipMask.None); Assert.Equal( new[] { (leftRing, (uint)EquipMask.FingerWearRight) }, h.Wields); Assert.Equal( EquipMask.FingerWearRight, h.Objects.Get(leftRing)!.CurrentlyEquippedLocation); } [Fact] public void BlocksUseOfShield_matchesRetailCombatUseAndAmmoRules() { Assert.True(AutoWieldController.BlocksUseOfShield(new ClientObject { CombatUse = 2, AmmoType = 1, })); Assert.False(AutoWieldController.BlocksUseOfShield(new ClientObject { CombatUse = 2, AmmoType = 0, })); Assert.True(AutoWieldController.BlocksUseOfShield(new ClientObject { CombatUse = 5, })); Assert.True(AutoWieldController.BlocksUseOfShield(new ClientObject { Type = ItemType.Caster, })); } [Fact] public void InventoryDragOutsideUi_sendsDropAndMovesToWorldOptimistically() { var h = new Harness(); h.AddContained(0x50000A07u); var payload = new ItemDragPayload( 0x50000A07u, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.True(h.Controller.DropToWorld(payload)); Assert.Equal(new[] { 0x50000A07u }, h.Drops); Assert.Equal(0u, h.Objects.Get(0x50000A07u)!.ContainerId); } [Fact] public void InventoryDragOnNpc_sendsGiveWithoutOptimisticInventoryMutation() { var h = new Harness(); const uint item = 0x50000A71u; const uint npc = 0x50001001u; h.AddContained(item); h.Objects.AddOrUpdate(new ClientObject { ObjectId = npc, Name = "Starter Exit Warden", Type = ItemType.Creature, }); var payload = new ItemDragPayload( item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.True(h.Controller.PlaceIn3D(payload, npc)); Assert.Equal(new[] { (npc, item, 1u) }, h.Gives); Assert.Empty(h.Drops); Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId); Assert.Equal(1, h.Objects.Get(item)!.StackSize); } [Fact] public void SelectedPartialStackDragOnNpc_sendsSelectedGiveAmount() { var h = new Harness(); const uint item = 0x50000A72u; const uint npc = 0x50001002u; h.AddContained(item, value => value.StackSize = 10); h.Objects.AddOrUpdate(new ClientObject { ObjectId = npc, Type = ItemType.Creature, }); h.SelectedObject = item; h.SplitQuantity.Reset(10u); h.SplitQuantity.SetValue(3u); var payload = new ItemDragPayload( item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.True(h.Controller.PlaceIn3D(payload, npc)); Assert.Equal(new[] { (npc, item, 3u) }, h.Gives); Assert.Equal(10, h.Objects.Get(item)!.StackSize); Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId); } [Fact] public void InventoryDragOnNonCreature_usesRetailGroundFallback() { var h = new Harness(); const uint item = 0x50000A73u; const uint scenery = 0x50002001u; h.AddContained(item); h.Objects.AddOrUpdate(new ClientObject { ObjectId = scenery, Type = ItemType.Misc, }); var payload = new ItemDragPayload( item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.True(h.Controller.PlaceIn3D(payload, scenery)); Assert.Empty(h.Gives); Assert.Equal(new[] { item }, h.Drops); Assert.Equal(0u, h.Objects.Get(item)!.ContainerId); } [Theory] [InlineData(true, false)] [InlineData(false, true)] public void InventoryDragOnPlayer_honorsRetailSecureTradeOption( bool opensSecureTrade, bool sendsGive) { var h = new Harness { DragOnPlayerOpensSecureTrade = opensSecureTrade }; const uint item = 0x50000A74u; const uint targetPlayer = 0x50003001u; h.AddContained(item); h.Objects.AddOrUpdate(new ClientObject { ObjectId = targetPlayer, Type = ItemType.Creature, PublicWeenieBitfield = (uint)PublicWeenieFlags.Player, }); var payload = new ItemDragPayload( item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); bool result = h.Controller.PlaceIn3D(payload, targetPlayer); Assert.Equal(sendsGive, result); if (sendsGive) Assert.Equal(new[] { (targetPlayer, item, 1u) }, h.Gives); else { Assert.Empty(h.Gives); Assert.Contains(h.Toasts, text => text.Contains("Secure trade", StringComparison.Ordinal)); } Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId); } [Fact] public void SelectedPartialStackDragOutsideUi_splitsWithoutMovingOriginal() { var h = new Harness(); h.AddContained(0x50000A70u, item => item.StackSize = 10); h.SelectedObject = 0x50000A70u; h.SplitQuantity.Reset(10u); h.SplitQuantity.SetValue(1u); var payload = new ItemDragPayload( 0x50000A70u, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.True(h.Controller.DropToWorld(payload)); Assert.Equal(new[] { (0x50000A70u, 1u) }, h.SplitDrops); Assert.Empty(h.Drops); Assert.Equal(Pack, h.Objects.Get(0x50000A70u)!.ContainerId); Assert.Equal(10, h.Objects.Get(0x50000A70u)!.StackSize); } [Fact] public void ToolbarShortcutDragOutsideUi_doesNotDropRealItem() { var h = new Harness(); h.AddContained(0x50000A08u); var payload = new ItemDragPayload( 0x50000A08u, ItemDragSource.ShortcutBar, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.False(h.Controller.DropToWorld(payload)); Assert.Empty(h.Drops); Assert.Equal(Pack, h.Objects.Get(0x50000A08u)!.ContainerId); } [Fact] public void ActivateItem_appliesRetailUseThrottle() { var h = new Harness(); h.AddContained(0x50000A09u, item => item.Useability = ItemUseability.Contained); h.Controller.ActivateItem(0x50000A09u); h.Controller.CompleteUse(0); h.Now += 199; h.Controller.ActivateItem(0x50000A09u); h.Now += 1; h.Controller.ActivateItem(0x50000A09u); Assert.Equal(new[] { 0x50000A09u, 0x50000A09u }, h.Uses); } [Fact] public void UseDone_releasesRetailBusyGate() { var h = new Harness(); h.AddContained(0x50000A0Au, item => item.Useability = ItemUseability.Contained); Assert.True(h.Controller.ActivateItem(0x50000A0Au)); h.Now += 200; Assert.False(h.Controller.ActivateItem(0x50000A0Au)); Assert.Equal(1, h.Controller.BusyCount); h.Controller.CompleteUse(0); h.Now += 200; // rejected busy attempt still advanced retail's throttle timestamp Assert.True(h.Controller.ActivateItem(0x50000A0Au)); Assert.Equal(2, h.Uses.Count); } }