diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index bbf718c65..3a8adafe1 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -536,6 +536,14 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert } } + // SendMobileIncoming (0x78) sends only the first item per layer (it dedupes via the + // `layers` span). Equipment Layer comes from tiledata, so e.g. a two-handed weapon and a + // shield both resolve to Layer.TwoHanded; a later same-layer item sent on its own via + // EquipUpdate (0x2E) or its OPL would leave the client holding two items on one equipment + // slot (a use-after-free on the legacy 2D client when that slot is later torn down). + // Returns true if this equipped item is such a later duplicate and must not be sent alone. + public bool IsDupedEquipLayer() => m_Parent is Mobile m && m.FindItemOnLayer(m_Layer) != this; + public List Items => LookupItems() ?? EmptyItems; public int LookupContainerVersion() => (this as Container)?._version ?? LookupCompactInfo()?.Version ?? 0; @@ -1387,6 +1395,14 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert return; } + // A second item sharing an equipment layer is omitted by SendMobileIncoming (0x78); + // sending it on its own via EquipUpdate/OPL would leave the client with two items on + // one slot. Skip the per-client sends entirely for the dupe. + if (IsDupedEquipLayer()) + { + return; + } + foreach (var state in map.GetClientsInRange(worldLoc, GetMaxUpdateRange())) { var m = state.Mobile; diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index f20d972b6..778d5b5a7 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -5187,14 +5187,18 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro } else if (item.Parent is Mobile) { - state.SendEquipUpdate(item); + // Don't re-show a duped-layer item on its own (see Item.IsDupedEquipLayer). + if (!item.IsDupedEquipLayer()) + { + state.SendEquipUpdate(item); + } } else { item.SendInfoTo(state); } - if (item.Parent != null) + if (item.Parent != null && !item.IsDupedEquipLayer()) { item.SendOPLPacketTo(state); }