/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Item.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using Server.Collections; using Server.ContextMenus; using Server.Items; using Server.Logging; using Server.Network; using Server.Targeting; namespace Server; /// /// Internal flags used to signal how the item should be updated and resent to nearby clients. /// [Flags] public enum ItemDelta { /// /// Nothing. /// None = 0x00000000, /// /// Resend the item. /// Update = 0x00000001, /// /// Resend the item only if it is equipped. /// EquipOnly = 0x00000002, /// /// Resend the item's properties. /// Properties = 0x00000004 } /// /// Enumeration containing possible ways to handle item ownership on death. /// public enum DeathMoveResult { /// /// The item should be placed onto the corpse. /// MoveToCorpse, /// /// The item should remain equipped. /// RemainEquipped, /// /// The item should be placed into the owners backpack. /// MoveToBackpack } /// /// Enumeration of an item's loot and steal state. /// public enum LootType : byte { /// /// Stealable. Lootable. /// Regular = 0, /// /// Unstealable. Unlootable, unless owned by a murderer. /// Newbied = 1, /// /// Unstealable. Unlootable, always. /// Blessed = 2, /// /// Stealable. Lootable, always. /// Cursed = 3 } public class BounceInfo { public BounceInfo(Item item) { Map = item.Map; Location = item.Location; WorldLoc = item.GetWorldLocation(); Parent = item.Parent; } private BounceInfo(Map map, Point3D loc, Point3D worldLoc, IEntity parent) { Map = map; Location = loc; WorldLoc = worldLoc; Parent = parent; } public Point3D Location { get; set; } public Point3D WorldLoc { get; set; } public Map Map { get; set; } public IEntity Parent { get; set; } public static BounceInfo Deserialize(IGenericReader reader) { if (reader.ReadBool()) { var map = reader.ReadMap(); var loc = reader.ReadPoint3D(); var worldLoc = reader.ReadPoint3D(); var parent = reader.ReadEntity(); return new BounceInfo(map, loc, worldLoc, parent); } return null; } public static void Serialize(BounceInfo info, IGenericWriter writer) { if (info == null) { writer.Write(false); } else { writer.Write(true); writer.Write(info.Map); writer.Write(info.Location); writer.Write(info.WorldLoc); if (info.Parent is Mobile mobile) { writer.Write(mobile); } else if (info.Parent is Item item) { writer.Write(item); } else { writer.Write((Serial)0); } } } } public enum TotalType { Gold, Items, Weight } [Flags] public enum ExpandFlag { None = 0x000, Name = 0x001, Items = 0x002, Bounce = 0x004, Holder = 0x008, Blessed = 0x010, TempFlag = 0x020, SaveFlag = 0x040, Weight = 0x080, Spawner = 0x100 } public partial class Item : IHued, IComparable, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode { private static readonly ILogger logger = LogFactory.GetLogger(typeof(Item)); public const int QuestItemHue = 0x4EA; // Hmmmm... "for EA"? public static readonly List EmptyItems = new(); private static readonly Queue m_DeltaQueue = new(); private static int m_OpenSlots; private int m_Amount; private CompactInfo m_CompactInfo; private ItemDelta m_DeltaFlags; private Direction m_Direction; private ImplFlag m_Flags; // Where DecayScheduler tracks this item. Not serialized; PostDeserialize re-registers. internal sbyte DecaySlot = DecayScheduler.SlotNone; private int m_Hue; private int m_ItemID; private Layer m_Layer; private Point3D m_Location; private LootType m_LootType; private Map m_Map; private IEntity m_Parent; // Mobile, Item, or null=World private ObjectPropertyList m_PropertyList; [Constructible] public Item(int itemID = 0) { m_ItemID = itemID; Serial = World.NewItem; // Assigned directly: the Visible/Movable setters and SetLastMoved() would register the item // for decay while m_Map is still null, then unregister it once it is Map.Internal. m_Flags = ImplFlag.Visible | ImplFlag.Movable; Amount = 1; m_Map = Map.Internal; LastMoved = Core.Now; World.AddEntity(this); } public Item(Serial serial) => Serial = serial; public int TempFlags { get => LookupCompactInfo()?.m_TempFlags ?? 0; set { var info = AcquireCompactInfo(); info.m_TempFlags = value; if (info.m_TempFlags == 0) { VerifyCompactInfo(); } } } public int SavedFlags { get => LookupCompactInfo()?.m_SavedFlags ?? 0; set { var info = AcquireCompactInfo(); info.m_SavedFlags = value; if (info.m_SavedFlags == 0) { VerifyCompactInfo(); } } } // Sectors [IgnoreDupe] public Item Next { get; set; } [IgnoreDupe] public Item Previous { get; set; } [IgnoreDupe] public bool OnLinkList { get; set; } /// /// The who is currently holding this item. /// [IgnoreDupe] public Mobile HeldBy { get => LookupCompactInfo()?.m_HeldBy; set { var info = AcquireCompactInfo(); info.m_HeldBy = value; if (info.m_HeldBy == null) { VerifyCompactInfo(); } } } /// /// Overridable. Determines whether the item will show . /// public virtual bool DisplayWeight => Core.ML && (Movable || IsLockedDown || IsSecure || ItemData.Weight != 255); [CommandProperty(AccessLevel.GameMaster)] public LootType LootType { get => m_LootType; set { if (m_LootType != value) { m_LootType = value; if (DisplayLootType) { InvalidateProperties(); } } } } public static TimeSpan DefaultDecayTime { get; set; } = TimeSpan.FromHours(1.0); [CommandProperty(AccessLevel.GameMaster)] public virtual TimeSpan DecayTime => DefaultDecayTime; [CommandProperty(AccessLevel.GameMaster)] public virtual bool Decays => Movable && Visible && Spawner == null; public DateTime LastMoved { get; set; } [CommandProperty(AccessLevel.GameMaster)] public bool Stackable { get => GetFlag(ImplFlag.Stackable); set => SetFlag(ImplFlag.Stackable, value); } [CommandProperty(AccessLevel.GameMaster)] public bool Visible { get => GetFlag(ImplFlag.Visible); set { if (GetFlag(ImplFlag.Visible) != value) { SetFlag(ImplFlag.Visible, value); if (m_Map != null) { var worldLoc = GetWorldLocation(); var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket(); foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange())) { var m = state.Mobile; if (!m.CanSee(this) && m.InRange(worldLoc, GetUpdateRange(m))) { OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial); state.Send(removeEntity); } } } Delta(ItemDelta.Update); UpdateDecayRegistration(); } } } [CommandProperty(AccessLevel.GameMaster)] public bool Movable { get => GetFlag(ImplFlag.Movable); set { if (GetFlag(ImplFlag.Movable) != value) { SetFlag(ImplFlag.Movable, value); Delta(ItemDelta.Update); UpdateDecayRegistration(); } } } public virtual bool ForceShowProperties => false; public virtual bool HandlesOnMovement => false; public static int LockedDownFlag { get; set; } public static int SecureFlag { get; set; } [IgnoreDupe] public bool IsLockedDown { get => GetTempFlag(LockedDownFlag); set { SetTempFlag(LockedDownFlag, value); InvalidateProperties(); } } [IgnoreDupe] public bool IsSecure { get => GetTempFlag(SecureFlag); set { SetTempFlag(SecureFlag, value); InvalidateProperties(); } } public virtual bool IsVirtualItem => false; public virtual bool CanSeeStaffOnly(Mobile from) => from.AccessLevel > AccessLevel.Counselor; public virtual int LabelNumber { get { if (m_ItemID < 0x4000) { return 1020000 + m_ItemID; } return 1078872 + m_ItemID; } } [CommandProperty(AccessLevel.GameMaster)] public int TotalGold => GetTotal(TotalType.Gold); [CommandProperty(AccessLevel.GameMaster)] public int TotalItems => GetTotal(TotalType.Items); [CommandProperty(AccessLevel.GameMaster)] public int TotalWeight => GetTotal(TotalType.Weight); public virtual double DefaultWeight { get { if (m_ItemID < 0 || m_ItemID > TileData.MaxItemValue || this is BaseMulti) { return 0; } var weight = TileData.ItemTable[m_ItemID].Weight; if (weight is 255 or 0) { weight = 1; } return weight; } } [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public double Weight { get { var info = LookupCompactInfo(); return info is { m_Weight: >= 0 } ? info.m_Weight : DefaultWeight; } set { var defaultWeight = DefaultWeight; var info = LookupCompactInfo(); var oldWeight = info is { m_Weight: >= 0 } ? info.m_Weight : defaultWeight; if (oldWeight == value) { return; } if (value < 0 || defaultWeight == value) { info?.m_Weight = -1; VerifyCompactInfo(); } else { (info ?? AcquireCompactInfo()).m_Weight = value; } UpdateTotal(this, TotalType.Weight, PileWeight - (int)Math.Ceiling(oldWeight * Amount)); InvalidateProperties(); } } [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public int PileWeight => (int)Math.Ceiling(Weight * Amount); [Hue] [CommandProperty(AccessLevel.GameMaster)] public virtual int Hue { get => m_Hue; set { if (m_Hue != value) { m_Hue = value; Delta(ItemDelta.Update); } } } public virtual bool Nontransferable => QuestItem; [CommandProperty(AccessLevel.GameMaster)] public virtual Layer Layer { get => m_Layer; set { if (m_Layer != value) { m_Layer = value; Delta(ItemDelta.EquipOnly); } } } // 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; [CommandProperty(AccessLevel.GameMaster)] public IEntity RootParent { get { var p = m_Parent; while (p is Item item) { if (item.m_Parent == null) { break; } p = item.m_Parent; } return p; } } public bool NoMoveHS { get; set; } public virtual int PhysicalResistance => 0; public virtual int FireResistance => 0; public virtual int ColdResistance => 0; public virtual int PoisonResistance => 0; public virtual int EnergyResistance => 0; [CommandProperty(AccessLevel.GameMaster)] public virtual int ItemID { get => m_ItemID; set { if (m_ItemID != value) { var oldPileWeight = PileWeight; m_ItemID = value; var newPileWeight = PileWeight; UpdateTotal(this, TotalType.Weight, newPileWeight - oldPileWeight); InvalidateProperties(); Delta(ItemDelta.Update); } } } public virtual string DefaultName => null; [CommandProperty(AccessLevel.GameMaster)] public string Name { get => LookupCompactInfo()?.m_Name ?? DefaultName; set { var info = LookupCompactInfo(); if (value == null || value == DefaultName) { info?.m_Name = null; VerifyCompactInfo(); } else { (info ?? AcquireCompactInfo()).m_Name = value; } InvalidateProperties(); } } // Note: Setting the parent via command/props causes problems. [IgnoreDupe] [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public IEntity Parent { get => m_Parent; set { if (m_Parent == value) { return; } var oldParent = m_Parent; if (m_Map != null && oldParent == null && value != null) { m_Map.OnLeave(this); } m_Parent = value; if (m_Map != null && oldParent != null && value == null) { m_Map.OnEnter(this); } } } [CommandProperty(AccessLevel.GameMaster)] public LightType Light { get => (LightType)m_Direction; set { if ((LightType)m_Direction != value) { m_Direction = (Direction)value; Delta(ItemDelta.Update); } } } [CommandProperty(AccessLevel.GameMaster)] public Direction Direction { get => m_Direction; set { if (m_Direction != value) { m_Direction = value; Delta(ItemDelta.Update); } } } [CommandProperty(AccessLevel.GameMaster)] public int Amount { get => m_Amount; set { var oldValue = m_Amount; if (oldValue != value) { var oldPileWeight = PileWeight; m_Amount = value; if (m_Amount <= 0) { logger.Error( new Exception("Item.Amount <= 0 error"), "Item {Type} ({Serial}) was changed to amount of {Amount}, but must be at least 1", GetType(), Serial, m_Amount ); } var newPileWeight = PileWeight; UpdateTotal(this, TotalType.Weight, newPileWeight - oldPileWeight); OnAmountChange(oldValue); Delta(ItemDelta.Update); if (oldValue > 1 || value > 1) { InvalidateProperties(); } if (!Stackable && m_Amount > 1) { logger.Warning( "{Serial}: Amount changed for non-stackable item '{Name}'. ({Amount})", Serial, m_Amount, GetType().Name ); } } } } public virtual bool HandlesOnSpeech => false; public virtual bool BlocksFit => false; public bool InSecureTrade => GetSecureTradeCont() != null; public ItemData ItemData => TileData.ItemTable[m_ItemID & TileData.MaxItemValue]; public virtual bool CanTarget => true; public virtual bool DisplayLootType => true; public static bool ScissorCopyLootType { get; set; } [CommandProperty(AccessLevel.GameMaster)] public bool QuestItem { get => GetFlag(ImplFlag.QuestItem); set { SetFlag(ImplFlag.QuestItem, value); InvalidateProperties(); Delta(ItemDelta.Update); } } public bool Insured { get => GetFlag(ImplFlag.Insured); set { SetFlag(ImplFlag.Insured, value); InvalidateProperties(); } } public bool PaidInsurance { get => GetFlag(ImplFlag.PaidInsurance); set => SetFlag(ImplFlag.PaidInsurance, value); } public Mobile BlessedFor { get => LookupCompactInfo()?.m_BlessedFor; set { var info = AcquireCompactInfo(); info.m_BlessedFor = value; if (info.m_BlessedFor == null) { VerifyCompactInfo(); } InvalidateProperties(); } } public int CompareTo(Item other) => other == null ? -1 : Serial.CompareTo(other.Serial); public virtual int HuedItemID => m_ItemID; public ObjectPropertyList PropertyList => m_PropertyList ??= InitializePropertyList(new ObjectPropertyList(this)); /// /// Overridable. Fills an with everything applicable. By default, this invokes /// , then Item.GetChildProperties or /// Mobile.GetChildProperties. This method should be overridden to add any /// custom /// properties. /// public virtual void GetProperties(IPropertyList list) { AddNameProperties(list); AppendChildNameProperties(list); } [IgnoreDupe] [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; [IgnoreDupe] [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } public virtual void Serialize(IGenericWriter writer) { writer.Write(9); // version var flags = SaveFlag.None; int x = m_Location.m_X, y = m_Location.m_Y, z = m_Location.m_Z; if (x != 0 || y != 0 || z != 0) { if (x is >= short.MinValue and <= short.MaxValue && y is >= short.MinValue and <= short.MaxValue && z is >= sbyte.MinValue and <= sbyte.MaxValue) { if (x != 0 || y != 0) { if (x is >= byte.MinValue and <= byte.MaxValue && y is >= byte.MinValue and <= byte.MaxValue) { flags |= SaveFlag.LocationByteXY; } else { flags |= SaveFlag.LocationShortXY; } } if (z != 0) { flags |= SaveFlag.LocationSByteZ; } } else { flags |= SaveFlag.LocationFull; } } var info = LookupCompactInfo(); var items = LookupItems(); if (m_Direction != Direction.North) { flags |= SaveFlag.Direction; } if (info?.m_Bounce != null) { flags |= SaveFlag.Bounce; } if (m_LootType != LootType.Regular) { flags |= SaveFlag.LootType; } if (m_ItemID != 0) { flags |= SaveFlag.ItemID; } if (m_Hue != 0) { flags |= SaveFlag.Hue; } if (m_Amount != 1) { flags |= SaveFlag.Amount; } if (m_Layer != Layer.Invalid) { flags |= SaveFlag.Layer; } if (info?.m_Name != null) { flags |= SaveFlag.Name; } if (m_Parent != null) { flags |= SaveFlag.Parent; } if (items.Count > 0) { flags |= SaveFlag.Items; } if (m_Map != Map.Internal) { flags |= SaveFlag.Map; } // if (m_InsuredFor != null && !m_InsuredFor.Deleted) // flags |= SaveFlag.InsuredFor; if (info != null) { if (info.m_BlessedFor?.Deleted == false) { flags |= SaveFlag.BlessedFor; } if (info.m_HeldBy?.Deleted == false) { flags |= SaveFlag.HeldBy; } if (info.m_SavedFlags != 0) { flags |= SaveFlag.SavedFlags; } } if (info == null || info.m_Weight < 0) { flags |= SaveFlag.NullWeight; } else if (info.m_Weight == 0.0) { flags |= SaveFlag.WeightIs0; } else if (info.m_Weight > 0) { if (info.m_Weight == (int)info.m_Weight) { flags |= SaveFlag.IntWeight; } else { flags |= SaveFlag.WeightNot1or0; } } var implFlags = m_Flags & (ImplFlag.Visible | ImplFlag.Movable | ImplFlag.Stackable | ImplFlag.Insured | ImplFlag.PaidInsurance | ImplFlag.QuestItem); if (implFlags != (ImplFlag.Visible | ImplFlag.Movable)) { flags |= SaveFlag.ImplFlags; } writer.Write((int)flags); /* begin last moved time optimization */ var ticks = LastMoved.Ticks; var now = Core.Now.Ticks; var minutes = new TimeSpan(now - ticks).TotalMinutes; writer.WriteEncodedInt((int)Math.Clamp(minutes, int.MinValue, int.MaxValue)); /* end */ if (GetSaveFlag(flags, SaveFlag.Direction)) { writer.Write((byte)m_Direction); } if (GetSaveFlag(flags, SaveFlag.Bounce)) { BounceInfo.Serialize(info?.m_Bounce, writer); } if (GetSaveFlag(flags, SaveFlag.LootType)) { writer.Write((byte)m_LootType); } if (GetSaveFlag(flags, SaveFlag.LocationFull)) { writer.WriteEncodedInt(x); writer.WriteEncodedInt(y); writer.WriteEncodedInt(z); } else { if (GetSaveFlag(flags, SaveFlag.LocationByteXY)) { writer.Write((byte)x); writer.Write((byte)y); } else if (GetSaveFlag(flags, SaveFlag.LocationShortXY)) { writer.Write((short)x); writer.Write((short)y); } if (GetSaveFlag(flags, SaveFlag.LocationSByteZ)) { writer.Write((sbyte)z); } } if (GetSaveFlag(flags, SaveFlag.ItemID)) { writer.WriteEncodedInt(m_ItemID); } if (GetSaveFlag(flags, SaveFlag.Hue)) { writer.WriteEncodedInt(m_Hue); } if (GetSaveFlag(flags, SaveFlag.Amount)) { writer.WriteEncodedInt(m_Amount); } if (GetSaveFlag(flags, SaveFlag.Layer)) { writer.Write((byte)m_Layer); } if (GetSaveFlag(flags, SaveFlag.Name)) { writer.Write(info.m_Name); } if (GetSaveFlag(flags, SaveFlag.Parent)) { writer.Write(m_Parent); } if (GetSaveFlag(flags, SaveFlag.Items)) { writer.Write(items); } if (GetSaveFlag(flags, SaveFlag.IntWeight)) { writer.WriteEncodedInt((int)info.m_Weight); } else if (GetSaveFlag(flags, SaveFlag.WeightNot1or0)) { writer.Write(info.m_Weight); } if (GetSaveFlag(flags, SaveFlag.Map)) { writer.Write(m_Map); } if (GetSaveFlag(flags, SaveFlag.ImplFlags)) { writer.WriteEncodedInt((int)implFlags); } if (GetSaveFlag(flags, SaveFlag.InsuredFor)) { writer.Write((Mobile)null); } if (GetSaveFlag(flags, SaveFlag.BlessedFor)) { writer.Write(info.m_BlessedFor); } if (GetSaveFlag(flags, SaveFlag.HeldBy)) { writer.Write(info.m_HeldBy); } if (GetSaveFlag(flags, SaveFlag.SavedFlags)) { writer.WriteEncodedInt(info.m_SavedFlags); } } public void MoveToWorld(WorldLocation worldLocation) { MoveToWorld(worldLocation.Location, worldLocation.Map); } /// /// Moves the Item to a given and . /// public void MoveToWorld(Point3D location, Map map) { if (Deleted) { return; } var oldLocation = GetWorldLocation(); var oldRealLocation = m_Location; // Register at the end instead: CanDecay() reads parent, map and location, none of which // are final yet. LastMoved = Core.Now; if (Parent is Mobile mobile) { mobile.RemoveItem(this); } else if (Parent is Item item) { item.RemoveItem(this); } if (m_Map != map) { var old = m_Map; if (m_Map != null) { m_Map.OnLeave(this); if (oldLocation.m_X != 0) { SendRemovePacket(oldLocation); } } m_Location = location; OnLocationChange(oldRealLocation); var items = LookupItems(); for (var i = 0; i < items.Count; ++i) { items[i].Map = map; } m_Map = map; m_Map?.OnEnter(this); OnMapChange(); if (m_Map != null) { var oldWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket(); var saWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket(); var hsWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket(); foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange())) { var m = state.Mobile; if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m))) { if (state.HighSeas) { var length = OutgoingEntityPackets.CreateWorldEntity(hsWorldItem, this, true); if (length != hsWorldItem.Length) { hsWorldItem = hsWorldItem[..length]; } SendInfoTo(state, hsWorldItem); } else if (state.StygianAbyss) { var length = OutgoingEntityPackets.CreateWorldEntity(saWorldItem, this, false); if (length != saWorldItem.Length) { saWorldItem = saWorldItem[..length]; } SendInfoTo(state, saWorldItem); } else { var length = OutgoingItemPackets.CreateWorldItem(oldWorldItem, this); if (length != oldWorldItem.Length) { oldWorldItem = oldWorldItem[..length]; } SendInfoTo(state, oldWorldItem); } } } } RemDelta(ItemDelta.Update); if (old == null || old == Map.Internal) { InvalidateProperties(); } } else if (m_Map != null) { if (oldLocation.m_X != 0) { var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket(); foreach (var state in m_Map.GetClientsInRange(oldLocation, GetMaxUpdateRange())) { var m = state.Mobile; if (!m.InRange(location, GetUpdateRange(m))) { OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial); state.Send(removeEntity); } } } var oldInternalLocation = m_Location; m_Location = location; OnLocationChange(oldRealLocation); foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange())) { var m = state.Mobile; if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m))) { SendInfoTo(state); } } m_Map.OnMove(oldInternalLocation, this); RemDelta(ItemDelta.Update); } else { Map = map; Location = location; } UpdateDecayRegistration(); } /// /// Has the item been deleted? /// public bool Deleted => GetFlag(ImplFlag.Deleted); [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public Map Map { get => m_Map; set { if (m_Map != value) { var old = m_Map; if (m_Map != null) { if (m_Parent == null) { m_Map.OnLeave(this); } SendRemovePacket(); } var items = LookupItems(); for (var i = 0; i < items.Count; ++i) { items[i].Map = value; } m_Map = value; if (m_Parent == null) { m_Map?.OnEnter(this); } Delta(ItemDelta.Update); OnMapChange(); if (old == null || old == Map.Internal) { InvalidateProperties(); } } } } public virtual void ProcessDelta() { var flags = m_DeltaFlags; SetFlag(ImplFlag.InQueue, false); m_DeltaFlags = ItemDelta.None; var map = m_Map; if (map == null || Deleted) { return; } var worldLoc = GetWorldLocation(); var update = (flags & ItemDelta.Update) != 0; if (update && m_Parent is Container { IsPublicContainer: false } contParent) { var rootParent = contParent.RootParent as Mobile; Mobile tradeRecip = null; if (rootParent != null) { var ns = rootParent.NetState; if (ns != null && rootParent.CanSee(this) && rootParent.InRange(worldLoc, GetUpdateRange(rootParent))) { ns.SendContainerContentUpdate(this); SendOPLPacketTo(ns); } } var st = GetSecureTradeCont()?.Trade; if (st != null) { var test = st.From.Mobile; if (test != null && test != rootParent) { tradeRecip = test; } test = st.To.Mobile; if (test != null && test != rootParent) { tradeRecip = test; } var ns = tradeRecip?.NetState; if (ns != null && tradeRecip.CanSee(this) && tradeRecip.InRange(worldLoc, GetUpdateRange(tradeRecip))) { ns.SendContainerContentUpdate(this); SendOPLPacketTo(ns); } } var openers = contParent.Openers; if (openers != null) { for (var i = 0; i < openers.Count; ++i) { var mob = openers[i]; var range = GetUpdateRange(mob); if (mob.Map != map || !mob.InRange(worldLoc, range)) { openers.RemoveAt(i--); } else { if (mob == rootParent || mob == tradeRecip) { continue; } var ns = mob.NetState; if (ns != null && mob.CanSee(this)) { ns.SendContainerContentUpdate(this); SendOPLPacketTo(ns); } } } if (openers.Count == 0) { contParent.Openers = null; } } 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; if (!m.CanSee(this) || !m.InRange(worldLoc, GetUpdateRange(m))) { continue; } if (update) { if (m_Parent == null) { SendInfoTo(state); } else { if (m_Parent is Item) { // TODO: Optimize by writing once? state.SendContainerContentUpdate(this); } else if (m_Parent is Mobile) { // TODO: Optimize by writing once? state.SendEquipUpdate(this); } SendOPLPacketTo(state); } } else if ((flags & ItemDelta.EquipOnly) != 0 && m_Parent is Mobile) { state.SendEquipUpdate(this); SendOPLPacketTo(state); } else if (ObjectPropertyList.Enabled && (flags & ItemDelta.Properties) != 0) { SendOPLPacketTo(state); } } } public virtual void Delete() { if (Deleted) { return; } OnDelete(); DecayScheduler.Unregister(this); var items = LookupItems(); for (var i = items.Count - 1; i >= 0; --i) { if (i < items.Count) { items[i].OnParentDeleted(this); } } SendRemovePacket(); SetFlag(ImplFlag.Deleted, true); if (Parent is Mobile mobile) { mobile.RemoveItem(this); } else if (Parent is Item item) { item.RemoveItem(this); } ClearBounce(); if (m_Map != null) { if (m_Parent == null) { m_Map.OnLeave(this); } m_Map = null; } World.RemoveEntity(this); OnAfterDelete(); ClearProperties(); } public virtual bool SkipSerialization => false; [IgnoreDupe] public ISpawner Spawner { get => LookupCompactInfo()?.m_Spawner; set { var info = AcquireCompactInfo(); var oldValue = info.m_Spawner; info.m_Spawner = value; if (info.m_Spawner == null) { VerifyCompactInfo(); } if (oldValue != value) { UpdateDecayRegistration(); } } } public virtual void OnBeforeSpawn(Point3D location, Map m) { } public virtual void OnAfterSpawn() { } [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public virtual Point3D Location { get => m_Location; set { var oldLocation = m_Location; if (oldLocation == value) { return; } if (m_Map != null) { if (m_Parent == null) { if (m_Location.m_X != 0) { var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket(); foreach (var state in m_Map.GetClientsInRange(oldLocation, GetMaxUpdateRange())) { var m = state.Mobile; if (!m.InRange(value, GetUpdateRange(m))) { OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial); state.Send(removeEntity); } } } var oldLoc = m_Location; m_Location = value; SetLastMoved(); foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange())) { var m = state.Mobile; if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m)) && (!state.HighSeas || !NoMoveHS || (m_DeltaFlags & ItemDelta.Update) != 0 || !m.InRange(oldLoc, GetUpdateRange(m)))) { SendInfoTo(state); } } RemDelta(ItemDelta.Update); } else { m_Location = value; if (m_Parent is Item) { Delta(ItemDelta.Update); } } if (m_Parent == null) { m_Map.OnMove(oldLocation, this); } } else { m_Location = value; } OnLocationChange(oldLocation); } } [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public int X { get => Location.m_X; set => Location = new Point3D(value, m_Location.m_Y, m_Location.m_Z); } [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public int Y { get => Location.m_Y; set => Location = new Point3D(m_Location.m_X, value, m_Location.m_Z); } [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public int Z { get => Location.m_Z; set => Location = new Point3D(m_Location.m_X, m_Location.m_Y, value); } public virtual bool InRange(Point2D p, int range) => Utility.InRange(p.X, p.Y, X, Y, range); public virtual bool InRange(Point3D p, int range) => Utility.InRange(p.X, p.Y, X, Y, range); public ExpandFlag GetExpandFlags() { var info = LookupCompactInfo(); ExpandFlag flags = 0; if (info != null) { if (info.m_BlessedFor != null) { flags |= ExpandFlag.Blessed; } if (info.m_Bounce != null) { flags |= ExpandFlag.Bounce; } if (info.m_HeldBy != null) { flags |= ExpandFlag.Holder; } if (info.m_Items != null) { flags |= ExpandFlag.Items; } if (info.m_Name != null) { flags |= ExpandFlag.Name; } if (info.m_Spawner != null) { flags |= ExpandFlag.Spawner; } if (info.m_SavedFlags != 0) { flags |= ExpandFlag.SaveFlag; } if (info.m_TempFlags != 0) { flags |= ExpandFlag.TempFlag; } if (info.m_Weight >= 0) { flags |= ExpandFlag.Weight; } } return flags; } private CompactInfo LookupCompactInfo() => m_CompactInfo; private CompactInfo AcquireCompactInfo() => m_CompactInfo ??= new CompactInfo(); private void ReleaseCompactInfo() { m_CompactInfo = null; } private void VerifyCompactInfo() { var info = m_CompactInfo; if (info == null) { return; } var isValid = info.m_Name != null || info.m_Items != null || info.m_Bounce != null || info.m_HeldBy != null || info.m_BlessedFor != null || info.m_Spawner != null || info.m_TempFlags != 0 || info.m_SavedFlags != 0 || info.m_Weight >= 0; if (!isValid) { ReleaseCompactInfo(); } } public List LookupItems() => (this is Container container ? container._items : LookupCompactInfo()?.m_Items) ?? EmptyItems; public List AcquireItems() { if (this is Container cont) { return cont._items ??= new List(); } var info = AcquireCompactInfo(); return info.m_Items ??= new List(); } private void SetFlag(ImplFlag flag, bool value) { if (value) { m_Flags |= flag; } else { m_Flags &= ~flag; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool GetFlag(ImplFlag flag) => (m_Flags & flag) != 0; public BounceInfo GetBounce() => LookupCompactInfo()?.m_Bounce; public void RecordBounce() { AcquireCompactInfo().m_Bounce = new BounceInfo(this); } public void ClearBounce() { var info = LookupCompactInfo(); var bounce = info?.m_Bounce; if (bounce == null) { return; } info.m_Bounce = null; if (bounce.Parent is Item parentItem) { if (!parentItem.Deleted) { parentItem.OnItemBounceCleared(this); } } else if (bounce.Parent is Mobile { Deleted: false } parentMobile) { parentMobile.OnItemBounceCleared(this); } VerifyCompactInfo(); } /// /// Overridable. Virtual event invoked when a client, , invokes a 'help request' for the Item. /// Seemingly no longer functional in newer clients. /// public virtual void OnHelpRequest(Mobile from) { } /// /// Overridable. Method checked to see if the item can be traded. /// /// True if the trade is allowed, false if not. public virtual bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) => true; /// /// Overridable. Virtual event invoked when a trade has completed, either successfully or not. /// public virtual void OnSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) { } /// /// Overridable. Method checked to see if the elemental resistances of this Item conflict with another Item on the /// . /// /// /// /// /// True /// /// There is a conflict. The elemental resistance bonuses of this Item should not be applied to the /// /// /// /// /// False /// There is no conflict. The bonuses should be applied. /// /// /// public virtual bool CheckPropertyConflict(Mobile m) => false; /// /// Overridable. Sends the object property list to . /// public virtual void SendPropertiesTo(NetState ns) { ns?.Send(PropertyList.Buffer); } /// /// Overridable. Adds the name of this item to the given . This method should be overridden /// if the item requires a complex naming format. /// public virtual void AddNameProperty(IPropertyList list) { var name = Name; if (name == null) { if (m_Amount <= 1) { list.Add(LabelNumber); } else { list.Add(1050039, $"{m_Amount}\t{LabelNumber:#}"); // ~1_NUMBER~ ~2_ITEMNAME~ } } else { if (m_Amount <= 1) { list.Add(name); } else { list.Add(1050039, $"{m_Amount}\t{Name}"); // ~1_NUMBER~ ~2_ITEMNAME~ } } } /// /// Overridable. Adds the loot type of this item to the given . By default, this will be /// either 'blessed', 'cursed', or 'insured'. /// public virtual void AddLootTypeProperty(IPropertyList list) { if (m_LootType == LootType.Blessed) { list.Add(1038021); // blessed } else if (m_LootType == LootType.Cursed) { list.Add(1049643); // cursed } else if (Insured) { list.Add(1061682); // insured } } /// /// Overridable. Adds any elemental resistances of this item to the given . /// public virtual void AddResistanceProperties(IPropertyList list) { var v = PhysicalResistance; if (v != 0) { list.Add(1060448, v); // physical resist ~1_val~% } v = FireResistance; if (v != 0) { list.Add(1060447, v); // fire resist ~1_val~% } v = ColdResistance; if (v != 0) { list.Add(1060445, v); // cold resist ~1_val~% } v = PoisonResistance; if (v != 0) { list.Add(1060449, v); // poison resist ~1_val~% } v = EnergyResistance; if (v != 0) { list.Add(1060446, v); // energy resist ~1_val~% } } /// /// Overridable. Displays cliloc 1072788-1072789. /// public virtual void AddWeightProperty(IPropertyList list) { var weight = PileWeight + TotalWeight; list.Add(weight == 1 ? 1072788 : 1072789, $"{weight}"); } /// /// Overridable. Adds header properties. By default, this invokes , /// (if applicable), and (if /// ). /// public virtual void AddNameProperties(IPropertyList list) { AddNameProperty(list); if (IsSecure) { AddSecureProperty(list); } else if (IsLockedDown) { AddLockedDownProperty(list); } var blessedFor = BlessedFor; if (blessedFor?.Deleted == false) { AddBlessedForProperty(list, blessedFor); } if (DisplayLootType) { AddLootTypeProperty(list); } if (DisplayWeight) { AddWeightProperty(list); } if (QuestItem) { AddQuestItemProperty(list); } } /// /// Overridable. Adds the "Quest Item" property to the given . /// public virtual void AddQuestItemProperty(IPropertyList list) { list.Add(1072351); // Quest Item } /// /// Overridable. Adds the "Locked Down & Secure" property to the given . /// public virtual void AddSecureProperty(IPropertyList list) { list.Add(501644); // locked down & secure } /// /// Overridable. Adds the "Locked Down" property to the given . /// public virtual void AddLockedDownProperty(IPropertyList list) { list.Add(501643); // locked down } /// /// Overridable. Adds the "Blessed for ~1_NAME~" property to the given . /// public virtual void AddBlessedForProperty(IPropertyList list, Mobile m) { list.Add(1062203, m.Name); // Blessed for ~1_NAME~ } /// /// Overridable. Event invoked when a child () is building it's . /// Recursively calls Item.GetChildProperties or /// Mobile.GetChildProperties. /// public virtual void GetChildProperties(IPropertyList list, Item item) { if (m_Parent is Item parentItem) { parentItem.GetChildProperties(list, item); } else if (m_Parent is Mobile parentMobile) { parentMobile.GetChildProperties(list, item); } } /// /// Overridable. Event invoked when a child () is building it's Name /// /// . Recursively calls Item.GetChildNameProperties or /// Mobile.GetChildNameProperties. /// public virtual void GetChildNameProperties(IPropertyList list, Item item) { if (m_Parent is Item parentItem) { parentItem.GetChildNameProperties(list, item); } else if (m_Parent is Mobile parentMobile) { parentMobile.GetChildNameProperties(list, item); } } public virtual bool IsChildVisibleTo(Mobile m, Item child) => true; public void Bounce(Mobile from) { m_Parent?.RemoveItem(this); m_Parent = null; var bounce = GetBounce(); if (bounce == null) { MoveToWorld(from.Location, from.Map); return; } var parent = bounce.Parent; if (parent?.Deleted != false) { MoveToWorld(from.Location, from.Map); return; } if (parent is Item ip) { var root = ip.RootParent; var rpm = root as Mobile; if (ip.IsAccessibleTo(from) && rpm?.CheckNonlocalDrop(from, this, ip) != false && (!ip.Movable || rpm == from || ip.Map == bounce.Map && root?.Location == bounce.WorldLoc) ) { Location = bounce.Location; ip.AddItem(this); } else { MoveToWorld(from.Location, from.Map); } } else if (parent is Mobile mobile) { if (!mobile.EquipItem(this)) { MoveToWorld(bounce.WorldLoc, bounce.Map); } } else { MoveToWorld(from.Location, from.Map); } ClearBounce(); } /// /// Overridable. Method checked to see if this item may be equipped while casting a spell. By default, this returns false. /// It /// is overridden on spellbook and spell channeling weapons or shields. /// /// True if it may, false if not. /// /// /// public override bool AllowEquippedCast( Mobile from ) /// { /// if (from.Int >= 100) /// return true; /// /// return base.AllowEquippedCast( from ); /// } /// When placed in an Item script, the item may be cast when equipped if the has 100 or more /// intelligence. Otherwise, it will drop to their backpack. /// public virtual bool AllowEquippedCast(Mobile from) => false; public virtual bool CheckConflictingLayer(Mobile m, Item item, Layer layer) => m_Layer == layer; // Uses Race.RaceFlag public virtual int RequiredRaces => Race.AllowAllRaces; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CheckRace(Race race) => Race.IsAllowedRace(race, RequiredRaces); public virtual bool CheckRace(Mobile from, bool message = true) { var race = from.Race; var requiredRaces = RequiredRaces; if (Race.IsAllowedRace(race, requiredRaces)) { return true; } if (!message) { return false; } if (requiredRaces == Race.AllowGargoylesOnly) { from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1111707); // Only gargoyles can wear this. } else if (race == Race.Gargoyle) { from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1111708); // Gargoyles can't wear this. } else if (requiredRaces == Race.AllowElvesOnly) { from.SendLocalizedMessage(1072203); // Only Elves may use this. } else { from.SendMessage($"{race.PluralName} may not use this."); } return false; } public virtual bool CanEquip(Mobile m) => m_Layer != Layer.Invalid && m.FindItemOnLayer(m_Layer) == null; public virtual void GetChildContextMenuEntries(Mobile from, ref PooledRefList list, Item item) { if (m_Parent is Item parentItem) { parentItem.GetChildContextMenuEntries(from, ref list, item); } else if (m_Parent is Mobile parentMobile) { parentMobile.GetChildContextMenuEntries(from, ref list, item); } } public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList list) { if (m_Parent is Item item) { item.GetChildContextMenuEntries(from, ref list, this); } else if (m_Parent is Mobile mobile) { mobile.GetChildContextMenuEntries(from, ref list, this); } } public virtual bool VerifyMove(Mobile from) => Movable; public virtual DeathMoveResult OnParentDeath(Mobile parent) { if (!Movable) { return DeathMoveResult.RemainEquipped; } if (parent.KeepsItemsOnDeath) { return DeathMoveResult.MoveToBackpack; } if (CheckBlessed(parent)) { return DeathMoveResult.MoveToBackpack; } if (CheckNewbied() && !parent.Murderer) { return DeathMoveResult.MoveToBackpack; } if (parent.Player && Nontransferable) { return DeathMoveResult.MoveToBackpack; } return DeathMoveResult.MoveToCorpse; } public virtual DeathMoveResult OnInventoryDeath(Mobile parent) { if (!Movable) { return DeathMoveResult.MoveToBackpack; } if (parent.KeepsItemsOnDeath) { return DeathMoveResult.MoveToBackpack; } if (CheckBlessed(parent)) { return DeathMoveResult.MoveToBackpack; } if (CheckNewbied() && !parent.Murderer) { return DeathMoveResult.MoveToBackpack; } if (parent.Player && Nontransferable) { return DeathMoveResult.MoveToBackpack; } return DeathMoveResult.MoveToCorpse; } /// /// Moves the Item to . The Item does not change maps. /// public virtual void MoveToWorld(Point3D location) { MoveToWorld(location, m_Map); } public void LabelTo(Mobile to, int number, string args = "") { to.NetState.SendMessageLocalized(Serial, m_ItemID, MessageType.Label, 0x3B2, 3, number, "", args); } public void LabelTo(Mobile to, string text) { to.NetState.SendMessage(Serial, m_ItemID, MessageType.Label, 0x3B2, 3, false, "ENU", "", text); } public void LabelToAffix(Mobile to, int number, AffixType type, string affix, string args = "") { to.NetState.SendMessageLocalizedAffix(Serial, m_ItemID, MessageType.Label, 0x3B2, 3, number, "", type, affix, args); } public virtual void LabelLootTypeTo(Mobile to) { if (m_LootType == LootType.Blessed) { LabelTo(to, 1041362); // (blessed) } else if (m_LootType == LootType.Cursed) { LabelTo(to, "(cursed)"); } } public bool AtWorldPoint(int x, int y) => m_Parent == null && m_Location.m_X == x && m_Location.m_Y == y; public bool AtPoint(int x, int y) => m_Location.m_X == x && m_Location.m_Y == y; public virtual bool CanDecay() => Decays && Parent == null && Map != Map.Internal; public virtual bool OnDecay() => CanDecay() && Region.Find(Location, Map).OnDecay(this); public DateTime ScheduledDecayTime => LastMoved + DecayTime; public void UpdateDecayRegistration() { DecayScheduler.Unregister(this); if (CanDecay()) { DecayScheduler.Register(this); } } public void SetLastMoved() { LastMoved = Core.Now; UpdateDecayRegistration(); } public virtual bool CanStackWith(Item dropped) => dropped.Stackable && Stackable && dropped.GetType() == GetType() && dropped.ItemID == ItemID && dropped.Hue == Hue && dropped.Name == Name && dropped.Amount + Amount <= 60000 && dropped != this; public bool StackWith(Mobile from, Item dropped) => StackWith(from, dropped, true); public virtual bool StackWith(Mobile from, Item dropped, bool playSound) { if (!CanStackWith(dropped)) { return false; } if (m_LootType != dropped.m_LootType) { m_LootType = LootType.Regular; } Amount += dropped.Amount; dropped.Delete(); if (playSound && from != null) { var soundID = GetDropSound(); if (soundID == -1) { soundID = 0x42; } from.SendSound(soundID, GetWorldLocation()); } return true; } public virtual bool OnDragDrop(Mobile from, Item dropped) { var success = (Parent as Container)?.OnStackAttempt(from, this, dropped) ?? StackWith(from, dropped); if (success && Spawner != null) { Spawner.Remove(this); Spawner = null; } return success; } public Rectangle2D GetGraphicBounds() { var itemID = m_ItemID; var doubled = m_Amount > 1; if (itemID is >= 0xEEA and <= 0xEF2) // Are we coins? { var coinBase = (itemID - 0xEEA) / 3; coinBase *= 3; coinBase += 0xEEA; doubled = false; itemID = m_Amount switch { <= 1 => coinBase, <= 5 => coinBase + 1, _ => coinBase + 2 }; } var bounds = ItemBounds.Bounds[itemID & 0x3FFF]; if (doubled) { bounds.Set(bounds.X, bounds.Y, bounds.Width + 5, bounds.Height + 5); } return bounds; } public virtual void AppendChildProperties(IPropertyList list) { if (m_Parent is Item item) { item.GetChildProperties(list, this); } else if (m_Parent is Mobile mobile) { mobile.GetChildProperties(list, this); } } public virtual void AppendChildNameProperties(IPropertyList list) { if (m_Parent is Item item) { item.GetChildNameProperties(list, this); } else if (m_Parent is Mobile mobile) { mobile.GetChildNameProperties(list, this); } } private ObjectPropertyList InitializePropertyList(ObjectPropertyList list) { GetProperties(list); AppendChildProperties(list); list.Terminate(); return list; } public virtual void ClearProperties() { m_PropertyList = null; } #nullable enable public virtual void InvalidateProperties() { if (!ObjectPropertyList.Enabled) { return; } if (m_Map != null && m_Map != Map.Internal && !World.Loading) { int? oldHash; int newHash; if (m_PropertyList != null) { oldHash = m_PropertyList.Hash; m_PropertyList.Reset(); InitializePropertyList(m_PropertyList); newHash = m_PropertyList.Hash; } else { oldHash = null; newHash = PropertyList.Hash; } if (oldHash != newHash) { Delta(ItemDelta.Properties); } } else { ClearProperties(); } } #nullable restore public virtual int GetPacketFlags() { var flags = 0; if (!Visible) { flags |= 0x80; } if (Movable || ForceShowProperties) { flags |= 0x20; } return flags; } public virtual bool OnMoveOff(Mobile m) => true; public virtual bool OnMoveOver(Mobile m) => true; public virtual void OnMovement(Mobile m, Point3D oldLocation) { } public void Internalize() { MoveToWorld(Point3D.Zero, Map.Internal); } public virtual void OnMapChange() { } public virtual void OnRemoved(IEntity parent) { } public virtual void OnAdded(IEntity parent) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool GetSaveFlag(SaveFlag flags, SaveFlag toGet) => (flags & toGet) != 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ItemAtEnumerable GetItemsAt() => m_Map == null ? Map.ItemAtEnumerable.Empty : m_Map.GetItemsAt(m_Parent == null ? m_Location : GetWorldLocation()); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ItemAtEnumerable GetItemsAt() where T : Item => m_Map == null ? Map.ItemAtEnumerable.Empty : m_Map.GetItemsAt(m_Parent == null ? m_Location : GetWorldLocation()); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ItemBoundsEnumerable GetItemsInRange(int range) => m_Map == null ? Map.ItemBoundsEnumerable.Empty : m_Map.GetItemsInRange(m_Parent == null ? m_Location : GetWorldLocation(), range); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ItemBoundsEnumerable GetItemsInRange(int range) where T : Item => m_Map == null ? Map.ItemBoundsEnumerable.Empty : m_Map.GetItemsInRange(m_Parent == null ? m_Location : GetWorldLocation(), range); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.MobileAtEnumerable GetMobilesAt() => GetMobilesAt(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.MobileAtEnumerable GetMobilesAt() where T : Mobile => m_Map == null ? Map.MobileAtEnumerable.Empty : m_Map.GetMobilesAt(m_Parent == null ? m_Location : GetWorldLocation()); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.MobileBoundsEnumerable GetMobilesInRange(int range) => GetMobilesInRange(range); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.MobileBoundsEnumerable GetMobilesInRange(int range) where T : Mobile => m_Map == null ? Map.MobileBoundsEnumerable.Empty : m_Map.GetMobilesInRange(m_Parent == null ? m_Location : GetWorldLocation(), range); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ClientAtEnumerable GetClientsAt() => m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Parent == null ? m_Location : GetWorldLocation()); [MethodImpl(MethodImplOptions.AggressiveInlining)] public Map.ClientBoundsEnumerable GetClientsInRange(int range) => m_Map == null ? Map.ClientBoundsEnumerable.Empty : Map.GetClientsInRange(m_Parent == null ? m_Location : GetWorldLocation(), range); public bool GetTempFlag(int flag) => ((LookupCompactInfo()?.m_TempFlags ?? 0) & flag) != 0; public void SetTempFlag(int flag, bool value) { var info = AcquireCompactInfo(); if (value) { info.m_TempFlags |= flag; } else { info.m_TempFlags &= ~flag; } if (info.m_TempFlags == 0) { VerifyCompactInfo(); } } public bool GetSavedFlag(int flag) => ((LookupCompactInfo()?.m_SavedFlags ?? 0) & flag) != 0; public void SetSavedFlag(int flag, bool value) { var info = AcquireCompactInfo(); if (value) { info.m_SavedFlags |= flag; } else { info.m_SavedFlags &= ~flag; } if (info.m_SavedFlags == 0) { VerifyCompactInfo(); } } public virtual void Deserialize(IGenericReader reader) { var version = reader.ReadInt(); // Default for versions without a saved value. Stamp only: map and parent are still unread, // and PostDeserialize registers every item once its state is final. LastMoved = Core.Now; switch (version) { case 9: case 8: case 7: case 6: { var flags = (SaveFlag)reader.ReadInt(); if (version < 7) { LastMoved = reader.ReadDeltaTime(); } else { var minutes = reader.ReadEncodedInt(); try { LastMoved = Core.Now - TimeSpan.FromMinutes(minutes); } catch { LastMoved = Core.Now; } } if (GetSaveFlag(flags, SaveFlag.Direction)) { m_Direction = (Direction)reader.ReadByte(); } if (GetSaveFlag(flags, SaveFlag.Bounce)) { AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader); } if (GetSaveFlag(flags, SaveFlag.LootType)) { m_LootType = (LootType)reader.ReadByte(); } int x = 0, y = 0, z = 0; if (GetSaveFlag(flags, SaveFlag.LocationFull)) { x = reader.ReadEncodedInt(); y = reader.ReadEncodedInt(); z = reader.ReadEncodedInt(); } else { if (GetSaveFlag(flags, SaveFlag.LocationByteXY)) { x = reader.ReadByte(); y = reader.ReadByte(); } else if (GetSaveFlag(flags, SaveFlag.LocationShortXY)) { x = reader.ReadShort(); y = reader.ReadShort(); } if (GetSaveFlag(flags, SaveFlag.LocationSByteZ)) { z = reader.ReadSByte(); } } m_Location = new Point3D(x, y, z); if (GetSaveFlag(flags, SaveFlag.ItemID)) { m_ItemID = reader.ReadEncodedInt(); } if (GetSaveFlag(flags, SaveFlag.Hue)) { m_Hue = reader.ReadEncodedInt(); } m_Amount = GetSaveFlag(flags, SaveFlag.Amount) ? reader.ReadEncodedInt() : 1; if (GetSaveFlag(flags, SaveFlag.Layer)) { m_Layer = (Layer)reader.ReadByte(); } if (GetSaveFlag(flags, SaveFlag.Name)) { var name = reader.ReadString(); if (name != DefaultName) { AcquireCompactInfo().m_Name = name; } } if (GetSaveFlag(flags, SaveFlag.Parent)) { var parent = reader.ReadSerial(); if (parent.IsMobile) { m_Parent = World.FindMobile(parent); } else if (parent.IsItem) { m_Parent = World.FindItem(parent); } else { m_Parent = null; } if (m_Parent == null && (parent.IsMobile || parent.IsItem)) { Timer.DelayCall(Delete); } } if (GetSaveFlag(flags, SaveFlag.Items)) { var items = reader.ReadEntityList(); if (this is Container) { (this as Container)._items = items; } else { AcquireCompactInfo().m_Items = items; } } if (version < 8 || !GetSaveFlag(flags, SaveFlag.NullWeight)) { double weight; if (GetSaveFlag(flags, SaveFlag.IntWeight)) { weight = reader.ReadEncodedInt(); } else if (GetSaveFlag(flags, SaveFlag.WeightNot1or0)) { weight = reader.ReadDouble(); } else if (GetSaveFlag(flags, SaveFlag.WeightIs0)) { weight = 0.0; } else { weight = 1.0; } if (weight != DefaultWeight) { AcquireCompactInfo().m_Weight = weight; } } m_Map = GetSaveFlag(flags, SaveFlag.Map) ? reader.ReadMap() : Map.Internal; SetFlag(ImplFlag.Visible, !GetSaveFlag(flags, SaveFlag.Visible) || reader.ReadBool()); SetFlag(ImplFlag.Movable, !GetSaveFlag(flags, SaveFlag.Movable) || reader.ReadBool()); if (GetSaveFlag(flags, SaveFlag.Stackable)) { SetFlag(ImplFlag.Stackable, reader.ReadBool()); } if (GetSaveFlag(flags, SaveFlag.ImplFlags)) { m_Flags = (ImplFlag)reader.ReadEncodedInt(); } if (GetSaveFlag(flags, SaveFlag.InsuredFor)) /*m_InsuredFor = */ { reader.ReadEntity(); } if (GetSaveFlag(flags, SaveFlag.BlessedFor)) { AcquireCompactInfo().m_BlessedFor = reader.ReadEntity(); } if (GetSaveFlag(flags, SaveFlag.HeldBy)) { AcquireCompactInfo().m_HeldBy = reader.ReadEntity(); } if (GetSaveFlag(flags, SaveFlag.SavedFlags)) { AcquireCompactInfo().m_SavedFlags = reader.ReadEncodedInt(); } if (m_Map != null && m_Parent == null) { m_Map.OnEnter(this); } break; } case 5: { var flags = (SaveFlag)reader.ReadInt(); LastMoved = reader.ReadDeltaTime(); if (GetSaveFlag(flags, SaveFlag.Direction)) { m_Direction = (Direction)reader.ReadByte(); } if (GetSaveFlag(flags, SaveFlag.Bounce)) { AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader); } if (GetSaveFlag(flags, SaveFlag.LootType)) { m_LootType = (LootType)reader.ReadByte(); } if (GetSaveFlag(flags, SaveFlag.LocationFull)) { m_Location = reader.ReadPoint3D(); } if (GetSaveFlag(flags, SaveFlag.ItemID)) { m_ItemID = reader.ReadInt(); } if (GetSaveFlag(flags, SaveFlag.Hue)) { m_Hue = reader.ReadInt(); } m_Amount = GetSaveFlag(flags, SaveFlag.Amount) ? reader.ReadInt() : 1; if (GetSaveFlag(flags, SaveFlag.Layer)) { m_Layer = (Layer)reader.ReadByte(); } if (GetSaveFlag(flags, SaveFlag.Name)) { var name = reader.ReadString(); if (name != DefaultName) { AcquireCompactInfo().m_Name = name; } } if (GetSaveFlag(flags, SaveFlag.Parent)) { var parent = reader.ReadSerial(); if (parent.IsMobile) { m_Parent = World.FindMobile(parent); } else if (parent.IsItem) { m_Parent = World.FindItem(parent); } else { m_Parent = null; } if (m_Parent == null && (parent.IsMobile || parent.IsItem)) { Timer.DelayCall(Delete); } } if (GetSaveFlag(flags, SaveFlag.Items)) { var items = reader.ReadEntityList(); if (this is Container cont) { cont._items = items; } else { AcquireCompactInfo().m_Items = items; } } double weight; if (GetSaveFlag(flags, SaveFlag.IntWeight)) { weight = reader.ReadEncodedInt(); } else if (GetSaveFlag(flags, SaveFlag.WeightNot1or0)) { weight = reader.ReadDouble(); } else if (GetSaveFlag(flags, SaveFlag.WeightIs0)) { weight = 0.0; } else { weight = 1.0; } if (weight != DefaultWeight) { AcquireCompactInfo().m_Weight = weight; } if (GetSaveFlag(flags, SaveFlag.Map)) { m_Map = reader.ReadMap(); } else { m_Map = Map.Internal; } SetFlag(ImplFlag.Visible, !GetSaveFlag(flags, SaveFlag.Visible) || reader.ReadBool()); SetFlag(ImplFlag.Movable, !GetSaveFlag(flags, SaveFlag.Movable) || reader.ReadBool()); if (GetSaveFlag(flags, SaveFlag.Stackable)) { SetFlag(ImplFlag.Stackable, reader.ReadBool()); } if (m_Map != null && m_Parent == null) { m_Map.OnEnter(this); } break; } case 4: // Just removed variables case 3: { m_Direction = (Direction)reader.ReadInt(); goto case 2; } case 2: { AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader); LastMoved = reader.ReadDeltaTime(); goto case 1; } case 1: { m_LootType = (LootType)reader.ReadByte(); // m_Newbied = reader.ReadBool(); goto case 0; } case 0: { m_Location = reader.ReadPoint3D(); m_ItemID = reader.ReadInt(); m_Hue = reader.ReadInt(); m_Amount = reader.ReadInt(); m_Layer = (Layer)reader.ReadByte(); var name = reader.ReadString(); if (name != DefaultName) { AcquireCompactInfo().m_Name = name; } var parent = reader.ReadSerial(); if (parent.IsMobile) { m_Parent = World.FindMobile(parent); } else if (parent.IsItem) { m_Parent = World.FindItem(parent); } else { m_Parent = null; } if (m_Parent == null && (parent.IsMobile || parent.IsItem)) { Timer.DelayCall(Delete); } var count = reader.ReadInt(); if (count > 0) { var items = new List(count); for (var i = 0; i < count; ++i) { var item = reader.ReadEntity(); if (item != null) { items.Add(item); } } if (this is Container cont) { cont._items = items; } else { AcquireCompactInfo().m_Items = items; } } var weight = reader.ReadDouble(); if (weight != DefaultWeight) { AcquireCompactInfo().m_Weight = weight; } if (version <= 3) { reader.ReadInt(); reader.ReadInt(); reader.ReadInt(); } m_Map = reader.ReadMap(); SetFlag(ImplFlag.Visible, reader.ReadBool()); SetFlag(ImplFlag.Movable, reader.ReadBool()); if (version <= 3) /*m_Deleted =*/ { reader.ReadBool(); } Stackable = reader.ReadBool(); if (m_Map != null && m_Parent == null) { m_Map.OnEnter(this); } break; } } if (HeldBy != null) { Timer.StartTimer(FixHolding_Sandbox); } // if (version < 9) VerifyCompactInfo(); } private void FixHolding_Sandbox() { var heldBy = HeldBy; if (heldBy != null) { if (GetBounce() != null) { Bounce(heldBy); } else { heldBy.Holding = null; heldBy.AddToBackpack(this); ClearBounce(); } } } public virtual int GetMaxUpdateRange() => 18; public virtual int GetUpdateRange(Mobile m) => 18; public virtual void SendInfoTo(NetState ns, ReadOnlySpan world = default) { SendWorldPacketTo(ns, world); SendOPLPacketTo(ns); } public virtual void SendOPLPacketTo(NetState ns) { if (ObjectPropertyList.Enabled) { ns.SendOPLInfo(this); } } public virtual void SendWorldPacketTo(NetState ns, ReadOnlySpan world = default) { if (world != ReadOnlySpan.Empty) { ns?.Send(world); return; } ns.SendWorldItem(this); } public virtual int GetTotal(TotalType type) => 0; public virtual void UpdateTotal(Item sender, TotalType type, int delta) { if (!IsVirtualItem) { if (m_Parent is Item item) { item.UpdateTotal(sender, type, delta); } else if (m_Parent is Mobile mobile) { mobile.UpdateTotal(sender, type, delta); } else { HeldBy?.UpdateTotal(sender, type, delta); } } } public virtual void UpdateTotals() { } public virtual void HandleInvalidTransfer(Mobile from) { // OSI sends 1074769, bug! if (QuestItem) { // You can only drop quest items into the top-most level of your backpack // while you still need them for your quest. from.SendLocalizedMessage(1049343); } } public bool ParentsContain() where T : Item { var p = m_Parent; while (p is Item item) { if (item is T) { return true; } if (item.m_Parent == null) { break; } p = item.m_Parent; } return false; } public virtual void AddItem(Item item) { if (item?.Deleted != false || item.m_Parent == this) { return; } if (item == this) { var customException = new InvalidOperationException("Adding item to itself"); logger.Warning( customException, "Adding item to itself: ({Serial1} {Item1}).AddItem({Serial2} {Item2})", Serial, GetType().Name, item.Serial, item.GetType().Name ); return; } if (IsChildOf(item)) { var customException = new InvalidOperationException("Adding parent item to child"); logger.Warning( customException, "Adding parent item to child: [{Serial1} {Item1}].AddItem( [{Serial2} {Item2}] )", Serial, GetType().Name, item.Serial, item.GetType().Name ); return; } if (item.m_Parent is Mobile parentMobile) { parentMobile.RemoveItem(item); } else if (item.m_Parent is Item parentItem) { parentItem.RemoveItem(item); } else { item.SendRemovePacket(); } item.Parent = this; item.Map = m_Map; var items = AcquireItems(); items.Add(item); if (this is not Container) { AcquireCompactInfo().Version++; } if (!item.IsVirtualItem) { UpdateTotal(item, TotalType.Gold, item.TotalGold); UpdateTotal(item, TotalType.Items, item.TotalItems + 1); UpdateTotal(item, TotalType.Weight, item.TotalWeight + item.PileWeight); } item.Delta(ItemDelta.Update); item.UpdateDecayRegistration(); item.OnAdded(this); OnItemAdded(item); } public void Delta(ItemDelta flags) { if (m_Map == null || m_Map == Map.Internal) { return; } m_DeltaFlags |= flags; if (!GetFlag(ImplFlag.InQueue)) { SetFlag(ImplFlag.InQueue, true); m_DeltaQueue.Enqueue(this); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RemDelta(ItemDelta flags) { m_DeltaFlags &= ~flags; } public static void ProcessDeltaQueue() { var limit = m_DeltaQueue.Count; while (m_DeltaQueue.Count > 0 && --limit >= 0) { var item = m_DeltaQueue.Dequeue(); if (item == null) { continue; } item.SetFlag(ImplFlag.InQueue, false); try { item.ProcessDelta(); } catch (Exception ex) { logger.Debug(ex, "Process Delta Queue for {Item} failed", item); } } if (m_DeltaQueue.Count > 0) { logger.Warning( "{Count} items left in delta queue after processing.", m_DeltaQueue.Count ); } } #if TRACK_LEAKS ~Item() { EntityFinalizationTracker.NotifyFinalized(this); } #endif public virtual void OnDelete() { if (Spawner != null) { Spawner.Remove(this); Spawner = null; } } public virtual void OnParentDeleted(IEntity parent) { Delete(); } public virtual void OnAfterDelete() { } public virtual void RemoveItem(Item item) { var items = LookupItems(); if (items.Remove(item)) { if (this is not Container) { AcquireCompactInfo().Version++; } item.SendRemovePacket(); if (!item.IsVirtualItem) { UpdateTotal(item, TotalType.Gold, -item.TotalGold); UpdateTotal(item, TotalType.Items, -(item.TotalItems + 1)); UpdateTotal(item, TotalType.Weight, -(item.TotalWeight + item.PileWeight)); } item.Parent = null; item.UpdateDecayRegistration(); item.OnRemoved(this); OnItemRemoved(item); } } public virtual void OnAfterDuped(Item newItem) { } // Warning: This uses reflection and is slow! public virtual void Dupe(Item newItem) { CopyProperties(this, newItem); OnAfterDuped(newItem); } // Warning: This uses reflection and is slow! public static void CopyProperties(Item src, Item dest) { var props = src.GetType().GetProperties(); for (var i = 0; i < props.Length; i++) { var p = props[i]; if (p.GetCustomAttribute(typeof(IgnoreDupeAttribute), true) != null || !p.CanRead || !p.CanWrite) { continue; } var setMethod = p.GetSetMethod(false); try { // Do not copy private properties if (setMethod != null) { p.SetValue(dest, p.GetValue(src, null), null); } } catch { // ignored } } } public virtual bool OnDragLift(Mobile from) => true; public virtual bool OnEquip(Mobile from) => true; protected virtual void OnAmountChange(int oldValue) { } public virtual void OnSpeech(SpeechEventArgs e) { } public virtual bool OnDroppedToMobile(Mobile from, Mobile target) { if (Nontransferable && from.Player) { HandleInvalidTransfer(from); return false; } return true; } public virtual bool DropToMobile(Mobile from, Mobile target, Point3D p) => !(Deleted || from.Deleted || target.Deleted) && from.Map == target.Map && from.Map != null && target.Map != null && (from.AccessLevel >= AccessLevel.GameMaster || from.InRange(target.Location, 2)) && from.CanSee(target) && from.InLOS(target) && from.OnDroppedItemToMobile(this, target) && OnDroppedToMobile(from, target) && target.OnDragDrop(from, this); public virtual bool OnDroppedInto(Mobile from, Container target, Point3D p) { if (!from.OnDroppedItemInto(this, target, p)) { return false; } if (Nontransferable && from.Player && target != from.Backpack) { HandleInvalidTransfer(from); return false; } return target.OnDragDropInto(from, this, p); } public virtual bool OnDroppedOnto(Mobile from, Item target) { if (Deleted || from.Deleted || target.Deleted || from.Map != target.Map || from.Map == null || target.Map == null) { return false; } if (from.AccessLevel < AccessLevel.GameMaster && !from.InRange(target.GetWorldLocation(), 2)) { return false; } if (!from.CanSee(target) || !from.InLOS(target)) { return false; } if (!target.IsAccessibleTo(from)) { return false; } if (!from.OnDroppedItemOnto(this, target)) { return false; } if (Nontransferable && from.Player && target != from.Backpack) { HandleInvalidTransfer(from); return false; } return target.OnDragDrop(from, this); } public virtual bool DropToItem(Mobile from, Item target, Point3D p) { if (Deleted || from.Deleted || target.Deleted || from.Map != target.Map || from.Map == null || target.Map == null) { return false; } if (from.AccessLevel < AccessLevel.GameMaster && !from.InRange(target.GetWorldLocation(), 2)) { return false; } if (!from.CanSee(target) || !from.InLOS(target)) { return false; } if (!target.IsAccessibleTo(from)) { return false; } if (target.RootParent is Mobile mobile && !mobile.CheckNonlocalDrop(from, this, target)) { return false; } if (!from.OnDroppedItemToItem(this, target, p)) { return false; } if (target is Container container && p.m_X != -1 && p.m_Y != -1) { return OnDroppedInto(from, container, p); } return OnDroppedOnto(from, target); } public virtual bool OnDroppedToWorld(Mobile from, Point3D p) { if (Nontransferable && from.Player) { HandleInvalidTransfer(from); return false; } return true; } public virtual int GetLiftSound(Mobile from) => 0x57; public virtual bool DropToWorld(Mobile from, Point3D p) { if (Deleted || from.Deleted || from.Map == null) { return false; } if (!from.InRange(p, 2)) { return false; } var map = from.Map; if (map == null) { return false; } int x = p.m_X, y = p.m_Y; var z = int.MinValue; var maxZ = from.Z + 16; var landTile = map.Tiles.GetLandTile(x, y); var landFlags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags; map.GetAverageZ(x, y, out var landZ, out var landAvg, out _); if (!landTile.Ignored && (landFlags & TileFlag.Impassable) == 0) { if (landAvg <= maxZ) { z = landAvg; } } foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; if (!id.Surface) { continue; } var top = tile.Z + id.CalcHeight; if (top > maxZ || top < z) { continue; } z = top; } using var items = PooledRefList.Create(); foreach (var item in map.GetItemsAt(p)) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue) { continue; } var id = item.ItemData; if (id.Surface) { var top = item.Z + id.CalcHeight; if (top <= maxZ && top >= z) { z = top; } } items.Add(item); } if (z == int.MinValue) { return false; } if (z > maxZ) { return false; } m_OpenSlots = (1 << 20) - 1; var surfaceZ = z; foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; var checkZ = tile.Z; var checkTop = checkZ + id.CalcHeight; if (checkTop == checkZ && !id.Surface) { ++checkTop; } var zStart = Math.Max(checkZ - z, 0); var zEnd = Math.Min(checkTop - z, 19); if (zStart >= 20 || zEnd < 0) { continue; } var bitCount = zEnd - zStart; m_OpenSlots &= ~(((1 << bitCount) - 1) << zStart); } for (var i = 0; i < items.Count; i++) { var item = items[i]; var id = item.ItemData; var checkZ = item.Z; var checkTop = checkZ + id.CalcHeight; if (checkTop == checkZ && !id.Surface) { ++checkTop; } var zStart = Math.Max(checkZ - z, 0); var zEnd = Math.Min(checkTop - z, 19); if (zStart >= 20 || zEnd < 0) { continue; } var bitCount = zEnd - zStart; m_OpenSlots &= ~(((1 << bitCount) - 1) << zStart); } var height = ItemData.Height; if (height == 0) { ++height; } if (height > 30) { height = 30; } var match = (1 << height) - 1; var okay = false; for (var i = 0; i < 20; ++i) { if (i + height > 20) { match >>= 1; } okay = ((m_OpenSlots >> i) & match) == match; if (okay) { z += i; break; } } if (!okay) { return false; } height = ItemData.Height; if (height == 0) { ++height; } if (landAvg > z && z + height > landZ) { return false; } if ((landFlags & TileFlag.Impassable) != 0 && landAvg > surfaceZ && z + height > landZ) { return false; } foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; var checkZ = tile.Z; var checkTop = checkZ + id.CalcHeight; if (checkTop > z && z + height > checkZ) { return false; } if ((id.Surface || id.Impassable) && checkTop > surfaceZ && z + height > checkZ) { return false; } } for (var i = 0; i < items.Count; ++i) { var item = items[i]; var id = item.ItemData; // int checkZ = item.Z; // int checkTop = checkZ + id.CalcHeight; if (item.Z + id.CalcHeight > z && z + height > item.Z) { return false; } } p = new Point3D(x, y, z); if (!from.InLOS(new Point3D(x, y, z + 1))) { return false; } if (!from.OnDroppedItemToWorld(this, p)) { return false; } if (!OnDroppedToWorld(from, p)) { return false; } var soundID = GetDropSound(); MoveToWorld(p, from.Map); from.SendSound(soundID == -1 ? 0x42 : soundID, GetWorldLocation()); return true; } public void SendRemovePacket() => SendRemovePacket(GetWorldLocation()); public void SendRemovePacket(Point3D worldLoc) { if (Deleted || m_Map == null) { return; } var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket(); foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange())) { var m = state.Mobile; if (m.InRange(worldLoc, GetUpdateRange(m))) { OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial); state.Send(removeEntity); } } } public virtual int GetDropSound() => -1; public Point3D GetWorldLocation() { var root = RootParent; if (root == null) { return m_Location; } return root.Location; // return root == null ? m_Location : new Point3D( (IPoint3D) root ); } public IPoint3D GetSurfaceTop() { var root = RootParent; return (root as Item)?.GetSurfaceTop() ?? (ItemData.Surface ? new Point3D( m_Location.m_X, m_Location.m_Y, m_Location.m_Z + ItemData.CalcHeight ) : this); } public Point3D GetWorldTop() { var root = RootParent; return (root as Item)?.GetWorldTop() ?? new Point3D( m_Location.m_X, m_Location.m_Y, m_Location.m_Z + ItemData.CalcHeight ); } public virtual void OnSnoop(Mobile from) { } public SecureTradeContainer GetSecureTradeCont() { object p = this; while (p is Item item) { if (item is SecureTradeContainer container) { return container; } p = item.m_Parent; } return null; } public virtual void OnItemAdded(Item item) { if (m_Parent is Item parentItem) { parentItem.OnSubItemAdded(item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnSubItemAdded(item); } } public virtual void OnItemRemoved(Item item) { if (m_Parent is Item parentItem) { parentItem.OnSubItemRemoved(item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnSubItemRemoved(item); } } public virtual void OnSubItemAdded(Item item) { if (m_Parent is Item parentItem) { parentItem.OnSubItemAdded(item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnSubItemAdded(item); } } public virtual void OnSubItemRemoved(Item item) { if (m_Parent is Item parentItem) { parentItem.OnSubItemRemoved(item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnSubItemRemoved(item); } } public virtual void OnItemBounceCleared(Item item) { if (m_Parent is Item parentItem) { parentItem.OnSubItemBounceCleared(item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnSubItemBounceCleared(item); } } public virtual void OnSubItemBounceCleared(Item item) { if (m_Parent is Item parentItem) { parentItem.OnSubItemBounceCleared(item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnSubItemBounceCleared(item); } } public virtual bool CheckTarget(Mobile from, Target targ, object targeted) => m_Parent switch { Item item => item.CheckTarget(from, targ, targeted), Mobile mobile => mobile.CheckTarget(from, targ, targeted), _ => true }; public virtual bool IsAccessibleTo(Mobile check) { if (m_Parent is Item item) { return item.IsAccessibleTo(check); } var reg = Region.Find(GetWorldLocation(), m_Map); return reg.CheckAccessibility(this, check); /*SecureTradeContainer cont = GetSecureTradeCont(); if (cont != null && !cont.IsChildOf( check )) return false; return true;*/ } public bool IsChildOf(IEntity o) => IsChildOf(o, false); public bool IsChildOf(IEntity o, bool allowNull) { var p = m_Parent; if ((p == null || o == null) && !allowNull) { return false; } if (p == o) { return true; } while (p is Item item) { if (item.m_Parent == null) { break; } p = item.m_Parent; if (p == o) { return true; } } return false; } public virtual void OnItemUsed(Mobile from, Item item) { if (m_Parent is Item parentItem) { parentItem.OnItemUsed(from, item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnItemUsed(from, item); } } public bool CheckItemUse(Mobile from) => CheckItemUse(from, this); public virtual bool CheckItemUse(Mobile from, Item item) => m_Parent switch { Item parentItem => parentItem.CheckItemUse(from, item), Mobile parentMobile => parentMobile.CheckItemUse(from, item), _ => true }; public virtual void OnItemLifted(Mobile from, Item item) { if (m_Parent is Item parentItem) { parentItem.OnItemLifted(from, item); } else if (m_Parent is Mobile parentMobile) { parentMobile.OnItemLifted(from, item); } } public bool CheckLift(Mobile from) { var reject = LRReason.Inspecific; return CheckLift(from, this, ref reject); } public virtual bool CheckLift(Mobile from, Item item, ref LRReason reject) => m_Parent switch { Item parentItem => parentItem.CheckLift(from, item, ref reject), Mobile parentMobile => parentMobile.CheckLift(from, item, ref reject), _ => true }; public virtual void OnSingleClickContained(Mobile from, Item item) { if (m_Parent is Item parentItem) { parentItem.OnSingleClickContained(from, item); } } public virtual void OnAosSingleClick(Mobile from) { var opl = PropertyList; if (opl.Header > 0) { from.NetState.SendMessageLocalized( Serial, m_ItemID, MessageType.Label, 0x3B2, 3, opl.Header, Name, opl.HeaderArgs ); } } public virtual void OnSingleClick(Mobile from) { if (Deleted || !from.CanSee(this)) { return; } if (DisplayLootType) { LabelLootTypeTo(from); } var ns = from.NetState; if (ns.CannotSendPackets()) { return; } if (Name == null) { if (m_Amount <= 1) { ns.SendMessageLocalized(Serial, m_ItemID, MessageType.Label, 0x3B2, 3, LabelNumber); } else { ns.SendMessageLocalizedAffix( Serial, m_ItemID, MessageType.Label, 0x3B2, 3, LabelNumber, "", AffixType.Append, $" : {m_Amount}" ); } } else if (m_Amount > 1) { ns.SendMessage( Serial, m_ItemID, MessageType.Label, 0x3B2, 3, false, "ENU", "", $"{Name} : {m_Amount}" ); } else { ns.SendMessage( Serial, m_ItemID, MessageType.Label, 0x3B2, 3, false, "ENU", "", $"{Name}" ); } } public virtual void ScissorHelper(Mobile from, Item newItem, int amountPerOldItem, bool carryHue = true) { // let's not go over 60000 var amount = Math.Min(Amount, 60000 / amountPerOldItem); var ourHue = Hue; var thisMap = Map; var thisParent = m_Parent; var worldLoc = GetWorldLocation(); var type = LootType; Consume(amount); newItem.Amount = amount * amountPerOldItem; if (carryHue) { newItem.Hue = ourHue; } if (ScissorCopyLootType) { newItem.LootType = type; } if ((thisParent as Container)?.TryDropItem(from, newItem, false) != true) { newItem.MoveToWorld(worldLoc, thisMap); } } public virtual void Consume(int amount = 1) { if (Amount <= amount) { Delete(); return; } Amount -= amount; } public virtual void ReplaceWith(Item newItem) { if (m_Parent is Container container) { container.AddItem(newItem); newItem.Location = m_Location; } else { newItem.MoveToWorld(GetWorldLocation(), m_Map); } Delete(); } public virtual bool CheckBlessed(Mobile m) => m_LootType == LootType.Blessed || Mobile.InsuranceEnabled && Insured || m != null && m == BlessedFor; public virtual bool CheckNewbied() => m_LootType == LootType.Newbied; public virtual bool IsStandardLoot() => (!Mobile.InsuranceEnabled || !Insured) && BlessedFor == null && m_LootType == LootType.Regular; public override string ToString() => $"{Serial} \"{GetType().Name}\""; public virtual void OnSectorActivate() { } public virtual void OnSectorDeactivate() { } public virtual void OnLocationChange(Point3D oldLocation) { } public virtual void OnDoubleClick(Mobile from) { } public virtual void OnDoubleClickOutOfRange(Mobile from) { } public virtual void OnDoubleClickCantSee(Mobile from) { } public virtual void OnDoubleClickDead(Mobile from) { from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019048); // I am dead and cannot do that. } public virtual void OnDoubleClickNotAccessible(Mobile from) { from.SendLocalizedMessage(500447); // That is not accessible. } public virtual void OnDoubleClickSecureTrade(Mobile from) { from.SendLocalizedMessage(500447); // That is not accessible. } [Flags] private enum ImplFlag : byte { None = 0x00, Visible = 0x01, Movable = 0x02, Deleted = 0x04, Stackable = 0x08, InQueue = 0x10, Insured = 0x20, PaidInsurance = 0x40, QuestItem = 0x80 } private class CompactInfo { public Mobile m_BlessedFor; public BounceInfo m_Bounce; public Mobile m_HeldBy; public List m_Items; public string m_Name; public int m_SavedFlags; public ISpawner m_Spawner; public int m_TempFlags; public double m_Weight = -1; public int Version; } [Flags] private enum SaveFlag : uint { None = 0x00000000, Direction = 0x00000001, Bounce = 0x00000002, LootType = 0x00000004, LocationFull = 0x00000008, ItemID = 0x00000010, Hue = 0x00000020, Amount = 0x00000040, Layer = 0x00000080, Name = 0x00000100, Parent = 0x00000200, Items = 0x00000400, WeightNot1or0 = 0x00000800, Map = 0x00001000, Visible = 0x00002000, Movable = 0x00004000, Stackable = 0x00008000, WeightIs0 = 0x00010000, LocationSByteZ = 0x00020000, LocationShortXY = 0x00040000, LocationByteXY = 0x00080000, ImplFlags = 0x00100000, InsuredFor = 0x00200000, BlessedFor = 0x00400000, HeldBy = 0x00800000, IntWeight = 0x01000000, SavedFlags = 0x02000000, NullWeight = 0x04000000 } }