From 410a9647891c517620695abaa5c33b3c2c6fe1c8 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:17:11 -0700 Subject: [PATCH] fix: NPC outfits no longer stack two items on one layer (#2715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to #2712, which covers HireRanger's refused `EquipItem`. A sweep that built every constructible Mobile 2,000 times under each expansion found these other outfits that put two items on one layer. They don't leak, because `AddItem` skips `CheckEquip`, but the NPC wears both items and the client draws only one. | NPC | Conflict | |---|---| | Miner, Ranger, Thief | Own shirt/pants/ThighBoots added on top of the random `BaseVendor.InitOutfit` (shoes doubled on every spawn) | | MasterTheophilus | Shoes + Robe added over `EvilMageLord`'s Sandals + Robe | | Merchant | Shirt + FancyShirt, or PlainDress + FancyDress | | IronWorker | Bandana/JesterHat + Bascinet | | MeerCaptain | One-handed sword + Crossbow | | BakeKitsune | `BodyMod` isn't saved, so after a restart `RemoveDisguise` bailed and the next `Disguise()` stacked a second robe and pair of shoes | ## Changes - `BaseCreature.ReplaceEquipment(item)` equips an item in place of whatever an inherited outfit put on its layer, and deletes the item if it still can't be equipped. - Miner, Ranger, Thief, MasterTheophilus: their own pieces replace the inherited ones. - Merchant and IronWorker: dropped the duplicate roll. Each kept the item the client was already drawing, which is the last one sent: the fancy set and the Bascinet. - MeerCaptain: the sword goes into its pack as loot. **Behavior change:** `Mobile.Weapon` checks the one-handed layer first, so it was fighting with the sword despite its archer AI, its Archery skill and the bolts in its pack. It now uses the crossbow. - BakeKitsune: `RemoveDisguise` strips the disguise without checking `IsBodyMod`. ## Testing - I re-ran the sweep (local diagnostic, not committed; needs client tiledata) at 500 rolls × 12 expansions. Only HireRanger (#2712) and LordOaks (separate fix coming) remain. - UOContent.Tests: 1218 passed, 2 skipped. --- Projects/UOContent/Mobiles/BaseCreature.cs | 14 ++++++++++++++ .../Mobiles/Monsters/LBR/Meers/MeerCaptain.cs | 3 ++- .../Mobiles/Monsters/ML/Bedlam/MasterTheophilus.cs | 4 ++-- .../UOContent/Mobiles/Monsters/SE/BakeKitsune.cs | 7 ++----- Projects/UOContent/Mobiles/Townfolk/Merchant.cs | 9 --------- .../UOContent/Mobiles/Vendors/NPC/IronWorker.cs | 8 -------- Projects/UOContent/Mobiles/Vendors/NPC/Miner.cs | 6 +++--- Projects/UOContent/Mobiles/Vendors/NPC/Ranger.cs | 6 +++--- Projects/UOContent/Mobiles/Vendors/NPC/Thief.cs | 6 +++--- 9 files changed, 29 insertions(+), 34 deletions(-) diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index bd28ff590..af619e005 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -5724,6 +5724,20 @@ namespace Server.Mobiles } } + /// + /// Equips in place of whatever an inherited outfit put on its layer. + /// AddItem would stack both on one layer, and EquipItem would refuse and orphan the item. + /// + public void ReplaceEquipment(Item item) + { + FindItemOnLayer(item.Layer)?.Delete(); + + if (!EquipItem(item)) + { + item.Delete(); + } + } + public virtual void HealStart(Mobile patient) { StopHeal(); diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs index d4b8eaf82..c7e7ab5d4 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerCaptain.cs @@ -47,7 +47,8 @@ namespace Server.Mobiles pack.DropItem(new Bolt(Utility.RandomMinMax(10, 20))); pack.DropItem(new Bolt(Utility.RandomMinMax(10, 20))); - AddItem( + // The crossbow is wielded; the sword would share its hands, so it is carried as loot. + pack.DropItem( Utility.Random(6) switch { 0 => new Longsword(), diff --git a/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/MasterTheophilus.cs b/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/MasterTheophilus.cs index fd3747095..d326916de 100644 --- a/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/MasterTheophilus.cs +++ b/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/MasterTheophilus.cs @@ -42,8 +42,8 @@ namespace Server.Mobiles Fame = 18000; Karma = -18000; - AddItem(new Shoes(0x537)); - AddItem(new Robe(0x452)); + ReplaceEquipment(new Shoes(0x537)); + ReplaceEquipment(new Robe(0x452)); for (var i = 0; i < 2; ++i) { diff --git a/Projects/UOContent/Mobiles/Monsters/SE/BakeKitsune.cs b/Projects/UOContent/Mobiles/Monsters/SE/BakeKitsune.cs index 96a8954d6..1e00abeb9 100644 --- a/Projects/UOContent/Mobiles/Monsters/SE/BakeKitsune.cs +++ b/Projects/UOContent/Mobiles/Monsters/SE/BakeKitsune.cs @@ -176,11 +176,8 @@ namespace Server.Mobiles { _disguiseTimerToken.Cancel(); - if (!IsBodyMod) - { - return; - } - + // BodyMod is not saved, so after a restart a disguised kitsune comes back without it but still + // wearing the disguise; strip it regardless. Name = null; Title = null; BodyMod = 0; diff --git a/Projects/UOContent/Mobiles/Townfolk/Merchant.cs b/Projects/UOContent/Mobiles/Townfolk/Merchant.cs index e5867c645..f9e5425df 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Merchant.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Merchant.cs @@ -33,15 +33,6 @@ public partial class Merchant : BaseEscortable public override void InitOutfit() { - if (Female) - { - AddItem(new PlainDress()); - } - else - { - AddItem(new Shirt(GetRandomHue())); - } - var lowHue = GetRandomHue(); AddItem(new ThighBoots()); diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/IronWorker.cs b/Projects/UOContent/Mobiles/Vendors/NPC/IronWorker.cs index e3ef3b2a5..a75e3efad 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/IronWorker.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/IronWorker.cs @@ -58,14 +58,6 @@ namespace Server.Mobiles AddItem(new FullApron(Utility.RandomBrightHue())); } - AddItem( - Utility.Random(3) switch - { - 0 => new Bandana(Utility.RandomBrightHue()), - _ => new JesterHat(Utility.RandomBrightHue()) // 1-2 - } - ); - AddItem(new Bascinet()); AddItem(new SmithHammer()); diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/Miner.cs b/Projects/UOContent/Mobiles/Vendors/NPC/Miner.cs index 3c8c33c77..9d8a439df 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/Miner.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/Miner.cs @@ -26,10 +26,10 @@ namespace Server.Mobiles { base.InitOutfit(); - AddItem(new FancyShirt(0x3E4)); - AddItem(new LongPants(0x192)); + ReplaceEquipment(new FancyShirt(0x3E4)); + ReplaceEquipment(new LongPants(0x192)); AddItem(new Pickaxe()); - AddItem(new ThighBoots(0x283)); + ReplaceEquipment(new ThighBoots(0x283)); } } } diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/Ranger.cs b/Projects/UOContent/Mobiles/Vendors/NPC/Ranger.cs index 7a4f499ba..4e8a9d462 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/Ranger.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/Ranger.cs @@ -31,10 +31,10 @@ namespace Server.Mobiles { base.InitOutfit(); - AddItem(new Shirt(Utility.RandomNeutralHue())); - AddItem(new LongPants(Utility.RandomNeutralHue())); + ReplaceEquipment(new Shirt(Utility.RandomNeutralHue())); + ReplaceEquipment(new LongPants(Utility.RandomNeutralHue())); AddItem(new Bow()); - AddItem(new ThighBoots(Utility.RandomNeutralHue())); + ReplaceEquipment(new ThighBoots(Utility.RandomNeutralHue())); } } } diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/Thief.cs b/Projects/UOContent/Mobiles/Vendors/NPC/Thief.cs index f9c802295..031bc4c97 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/Thief.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/Thief.cs @@ -31,10 +31,10 @@ namespace Server.Mobiles { base.InitOutfit(); - AddItem(new Shirt(Utility.RandomNeutralHue())); - AddItem(new LongPants(Utility.RandomNeutralHue())); + ReplaceEquipment(new Shirt(Utility.RandomNeutralHue())); + ReplaceEquipment(new LongPants(Utility.RandomNeutralHue())); AddItem(new Dagger()); - AddItem(new ThighBoots(Utility.RandomNeutralHue())); + ReplaceEquipment(new ThighBoots(Utility.RandomNeutralHue())); } } }