modernuo/Projects/Server/Items/Item.cs
Kamron Batman c75514cc04
feat: Updates to .NET 9 (#1984)
### Summary

* Bumps to .NET 9 with updated dependencies
* Comparing a value type against null is no longer allowed
* CI/CD now uses the version specified in global.json
* Serialization generator updated to .NET 9 with bug fixes, fixes to turkish language, and parallelization
2024-12-08 10:16:34 -08:00

4352 lines
114 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - 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 <http://www.gnu.org/licenses/>. *
*************************************************************************/
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;
/// <summary>
/// Internal flags used to signal how the item should be updated and resent to nearby clients.
/// </summary>
[Flags]
public enum ItemDelta
{
/// <summary>
/// Nothing.
/// </summary>
None = 0x00000000,
/// <summary>
/// Resend the item.
/// </summary>
Update = 0x00000001,
/// <summary>
/// Resend the item only if it is equipped.
/// </summary>
EquipOnly = 0x00000002,
/// <summary>
/// Resend the item's properties.
/// </summary>
Properties = 0x00000004
}
/// <summary>
/// Enumeration containing possible ways to handle item ownership on death.
/// </summary>
public enum DeathMoveResult
{
/// <summary>
/// The item should be placed onto the corpse.
/// </summary>
MoveToCorpse,
/// <summary>
/// The item should remain equipped.
/// </summary>
RemainEquipped,
/// <summary>
/// The item should be placed into the owners backpack.
/// </summary>
MoveToBackpack
}
/// <summary>
/// Enumeration of an item's loot and steal state.
/// </summary>
public enum LootType : byte
{
/// <summary>
/// Stealable. Lootable.
/// </summary>
Regular = 0,
/// <summary>
/// Unstealable. Unlootable, unless owned by a murderer.
/// </summary>
Newbied = 1,
/// <summary>
/// Unstealable. Unlootable, always.
/// </summary>
Blessed = 2,
/// <summary>
/// Stealable. Lootable, always.
/// </summary>
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();
IEntity parent = reader.ReadEntity<IEntity>();
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 class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode<Item>
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Item));
public const int QuestItemHue = 0x4EA; // Hmmmm... "for EA"?
public static readonly List<Item> EmptyItems = new();
private static readonly Queue<Item> 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;
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;
Visible = true;
Movable = true;
Amount = 1;
m_Map = Map.Internal;
SetLastMoved();
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; }
/// <summary>
/// The <see cref="Mobile" /> who is currently <see cref="Mobile.Holding">holding</see> this item.
/// </summary>
[IgnoreDupe]
public Mobile HeldBy
{
get => LookupCompactInfo()?.m_HeldBy;
set
{
var info = AcquireCompactInfo();
info.m_HeldBy = value;
if (info.m_HeldBy == null)
{
VerifyCompactInfo();
}
}
}
/// <summary>
/// Overridable. Determines whether the item will show <see cref="AddWeightProperty" />.
/// </summary>
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;
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();
Span<byte> 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);
}
}
}
[CommandProperty(AccessLevel.GameMaster)]
public bool Movable
{
get => GetFlag(ImplFlag.Movable);
set
{
if (GetFlag(ImplFlag.Movable) != value)
{
SetFlag(ImplFlag.Movable, value);
Delta(ItemDelta.Update);
}
}
}
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
{
if (Weight != value)
{
var info = AcquireCompactInfo();
var oldPileWeight = PileWeight;
info.m_Weight = value;
if (info.m_Weight < 0)
{
VerifyCompactInfo();
}
var newPileWeight = PileWeight;
UpdateTotal(this, TotalType.Weight, newPileWeight - oldPileWeight);
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);
}
}
}
public List<Item> Items => LookupItems() ?? EmptyItems;
[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
{
if (value == null || value != DefaultName)
{
var info = AcquireCompactInfo();
info.m_Name = value;
if (info.m_Name == null)
{
VerifyCompactInfo();
}
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));
/// <summary>
/// Overridable. Fills an <see cref="ObjectPropertyList" /> with everything applicable. By default, this invokes
/// <see cref="AddNameProperties" />, then <see cref="Item.GetChildProperties">Item.GetChildProperties</see> or
/// <see cref="Mobile.GetChildProperties">Mobile.GetChildProperties</see>. This method should be overridden to add any
/// custom
/// properties.
/// </summary>
public virtual void GetProperties(IPropertyList list)
{
AddNameProperties(list);
}
[IgnoreDupe]
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now;
[IgnoreDupe]
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
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);
}
/// <summary>
/// Moves the Item to a given <paramref name="location" /> and <paramref name="map" />.
/// </summary>
public void MoveToWorld(Point3D location, Map map)
{
if (Deleted)
{
return;
}
var oldLocation = GetWorldLocation();
var oldRealLocation = m_Location;
SetLastMoved();
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)
{
Span<byte> oldWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket();
Span<byte> saWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket();
Span<byte> 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)
{
Span<byte> 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;
}
}
/// <summary>
/// Has the item been deleted?
/// </summary>
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;
}
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();
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();
}
[IgnoreDupe]
public ISpawner Spawner
{
get => LookupCompactInfo()?.m_Spawner;
set
{
var info = AcquireCompactInfo();
info.m_Spawner = value;
if (info.m_Spawner == null)
{
VerifyCompactInfo();
}
}
}
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)
{
Span<byte> 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<Item> LookupItems() => (this is Container container ? container.m_Items : LookupCompactInfo()?.m_Items) ?? EmptyItems;
public List<Item> AcquireItems()
{
if (this is Container cont)
{
return cont.m_Items ?? (cont.m_Items = new List<Item>());
}
var info = AcquireCompactInfo();
return info.m_Items ?? (info.m_Items = new List<Item>());
}
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();
}
/// <summary>
/// Overridable. Virtual event invoked when a client, <paramref name="from" />, invokes a 'help request' for the Item.
/// Seemingly no longer functional in newer clients.
/// </summary>
public virtual void OnHelpRequest(Mobile from)
{
}
/// <summary>
/// Overridable. Method checked to see if the item can be traded.
/// </summary>
/// <returns>True if the trade is allowed, false if not.</returns>
public virtual bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) => true;
/// <summary>
/// Overridable. Virtual event invoked when a trade has completed, either successfully or not.
/// </summary>
public virtual void OnSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted)
{
}
/// <summary>
/// Overridable. Method checked to see if the elemental resistances of this Item conflict with another Item on the
/// <see cref="Mobile" />.
/// </summary>
/// <returns>
/// <list type="table">
/// <item>
/// <term>True</term>
/// <description>
/// There is a conflict. The elemental resistance bonuses of this Item should not be applied to the
/// <see cref="Mobile" />
/// </description>
/// </item>
/// <item>
/// <term>False</term>
/// <description>There is no conflict. The bonuses should be applied.</description>
/// </item>
/// </list>
/// </returns>
public virtual bool CheckPropertyConflict(Mobile m) => false;
/// <summary>
/// Overridable. Sends the <see cref="PropertyList">object property list</see> to <paramref name="from" />.
/// </summary>
public virtual void SendPropertiesTo(NetState ns)
{
ns?.Send(PropertyList.Buffer);
}
/// <summary>
/// Overridable. Adds the name of this item to the given <see cref="ObjectPropertyList" />. This method should be overridden
/// if the item requires a complex naming format.
/// </summary>
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~
}
}
}
/// <summary>
/// Overridable. Adds the loot type of this item to the given <see cref="ObjectPropertyList" />. By default, this will be
/// either 'blessed', 'cursed', or 'insured'.
/// </summary>
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); // <b>insured</b>
}
}
/// <summary>
/// Overridable. Adds any elemental resistances of this item to the given <see cref="ObjectPropertyList" />.
/// </summary>
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~%
}
}
/// <summary>
/// Overridable. Displays cliloc 1072788-1072789.
/// </summary>
public virtual void AddWeightProperty(IPropertyList list)
{
var weight = PileWeight + TotalWeight;
list.Add(weight == 1 ? 1072788 : 1072789, $"{weight}");
}
/// <summary>
/// Overridable. Adds header properties. By default, this invokes <see cref="AddNameProperty" />,
/// <see cref="AddBlessedForProperty" /> (if applicable), and <see cref="AddLootTypeProperty" /> (if
/// <see cref="DisplayLootType" />).
/// </summary>
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);
}
AppendChildNameProperties(list);
}
/// <summary>
/// Overridable. Adds the "Quest Item" property to the given <see cref="ObjectPropertyList" />.
/// </summary>
public virtual void AddQuestItemProperty(IPropertyList list)
{
list.Add(1072351); // Quest Item
}
/// <summary>
/// Overridable. Adds the "Locked Down & Secure" property to the given <see cref="ObjectPropertyList" />.
/// </summary>
public virtual void AddSecureProperty(IPropertyList list)
{
list.Add(501644); // locked down & secure
}
/// <summary>
/// Overridable. Adds the "Locked Down" property to the given <see cref="ObjectPropertyList" />.
/// </summary>
public virtual void AddLockedDownProperty(IPropertyList list)
{
list.Add(501643); // locked down
}
/// <summary>
/// Overridable. Adds the "Blessed for ~1_NAME~" property to the given <see cref="ObjectPropertyList" />.
/// </summary>
public virtual void AddBlessedForProperty(IPropertyList list, Mobile m)
{
list.Add(1062203, m.Name); // Blessed for ~1_NAME~
}
/// <summary>
/// Overridable. Event invoked when a child (<paramref name="item" />) is building it's <see cref="ObjectPropertyList" />.
/// Recursively calls <see cref="Item.GetChildProperties">Item.GetChildProperties</see> or
/// <see cref="Mobile.GetChildProperties">Mobile.GetChildProperties</see>.
/// </summary>
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);
}
}
/// <summary>
/// Overridable. Event invoked when a child (<paramref name="item" />) is building it's Name
/// <see cref="ObjectPropertyList" />
/// . Recursively calls <see cref="Item.GetChildProperties">Item.GetChildNameProperties</see> or
/// <see cref="Mobile.GetChildProperties">Mobile.GetChildNameProperties</see>.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <returns>True if it may, false if not.</returns>
/// <example>
/// <code>
/// public override bool AllowEquippedCast( Mobile from )
/// {
/// if (from.Int &gt;= 100)
/// return true;
///
/// return base.AllowEquippedCast( from );
/// }</code>
/// When placed in an Item script, the item may be cast when equipped if the <paramref name="from" /> has 100 or more
/// intelligence. Otherwise, it will drop to their backpack.
/// </example>
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<ContextMenuEntry> 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<ContextMenuEntry> 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.Kills < 5)
{
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.Kills < 5)
{
return DeathMoveResult.MoveToBackpack;
}
if (parent.Player && Nontransferable)
{
return DeathMoveResult.MoveToBackpack;
}
return DeathMoveResult.MoveToCorpse;
}
/// <summary>
/// Moves the Item to <paramref name="location" />. The Item does not change maps.
/// </summary>
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 void SetLastMoved()
{
LastMoved = Core.Now;
}
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.Table[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 void SetSaveFlag(ref SaveFlag flags, SaveFlag toSet, bool setIf)
{
if (setIf)
{
flags |= toSet;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetSaveFlag(SaveFlag flags, SaveFlag toGet) => (flags & toGet) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<Item> GetItemsAt() =>
m_Map == null ? Map.ItemAtEnumerable<Item>.Empty : m_Map.GetItemsAt(m_Parent == null ? m_Location : GetWorldLocation());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<T> GetItemsAt<T>() where T : Item =>
m_Map == null ? Map.ItemAtEnumerable<T>.Empty : m_Map.GetItemsAt<T>(m_Parent == null ? m_Location : GetWorldLocation());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemBoundsEnumerable<Item> GetItemsInRange(int range) =>
m_Map == null ? Map.ItemBoundsEnumerable<Item>.Empty : m_Map.GetItemsInRange(m_Parent == null ? m_Location : GetWorldLocation(), range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemBoundsEnumerable<T> GetItemsInRange<T>(int range) where T : Item =>
m_Map == null ? Map.ItemBoundsEnumerable<T>.Empty : m_Map.GetItemsInRange<T>(m_Parent == null ? m_Location : GetWorldLocation(), range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileAtEnumerable<Mobile> GetMobilesAt() => GetMobilesAt<Mobile>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileAtEnumerable<T> GetMobilesAt<T>() where T : Mobile =>
m_Map == null ? Map.MobileAtEnumerable<T>.Empty : m_Map.GetMobilesAt<T>(m_Parent == null ? m_Location : GetWorldLocation());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileBoundsEnumerable<Mobile> GetMobilesInRange(int range) => GetMobilesInRange<Mobile>(range);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.MobileBoundsEnumerable<T> GetMobilesInRange<T>(int range) where T : Mobile =>
m_Map == null ? Map.MobileBoundsEnumerable<T>.Empty : m_Map.GetMobilesInRange<T>(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();
SetLastMoved();
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))
{
Serial 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<Item>();
if (this is Container)
{
(this as Container).m_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<Mobile>();
}
if (GetSaveFlag(flags, SaveFlag.BlessedFor))
{
AcquireCompactInfo().m_BlessedFor = reader.ReadEntity<Mobile>();
}
if (GetSaveFlag(flags, SaveFlag.HeldBy))
{
AcquireCompactInfo().m_HeldBy = reader.ReadEntity<Mobile>();
}
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))
{
Serial 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<Item>();
if (this is Container cont)
{
cont.m_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;
}
Serial 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<Item>(count);
for (var i = 0; i < count; ++i)
{
var item = reader.ReadEntity<Item>();
if (item != null)
{
items.Add(item);
}
}
if (this is Container cont)
{
cont.m_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<byte> 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<byte> world = default)
{
if (world != ReadOnlySpan<byte>.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<T>() 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 (!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.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
);
}
}
public virtual void OnDelete()
{
if (Spawner != null)
{
Spawner.Remove(this);
Spawner = null;
}
}
public virtual void OnParentDeleted(IEntity parent)
{
Delete();
}
public void PublicOverheadMessage(MessageType type, int hue, bool ascii, string text)
{
if (m_Map == null)
{
return;
}
var worldLoc = GetWorldLocation();
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();
foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
{
var m = state.Mobile;
if (m.CanSee(this) && m.InRange(worldLoc, GetUpdateRange(m)))
{
var length = OutgoingMessagePackets.CreateMessage(
buffer, Serial, m_ItemID, type, hue, 3, ascii, "ENU", Name, text
);
if (length != buffer.Length)
{
buffer = buffer[..length]; // Adjust to the actual size
}
state.Send(buffer);
}
}
}
public void PublicOverheadMessage(MessageType type, int hue, int number, string args = "")
{
if (m_Map == null)
{
return;
}
var worldLoc = GetWorldLocation();
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedLength(args)].InitializePacket();
foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
{
var m = state.Mobile;
if (m.CanSee(this) && m.InRange(worldLoc, GetUpdateRange(m)))
{
var length = OutgoingMessagePackets.CreateMessageLocalized(
buffer, Serial, m_ItemID, type, hue, 3, number, Name, args
);
if (length != buffer.Length)
{
buffer = buffer[..length]; // Adjust to the actual size
}
state.Send(buffer);
}
}
}
public virtual void OnAfterDelete()
{
}
public virtual void RemoveItem(Item item)
{
var items = LookupItems();
if (items.Remove(item))
{
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.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<Item>.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;
}
Span<byte> 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 void SendLocalizedMessageTo(Mobile to, int number, string args = "")
{
if (Deleted || !to.CanSee(this))
{
return;
}
to.NetState.SendMessageLocalized(Serial, ItemID, MessageType.Regular, 0x3B2, 3, number, "", args);
}
public void SendLocalizedMessageTo(Mobile to, int number, AffixType affixType, string affix, string args)
{
if (Deleted || !to.CanSee(this))
{
return;
}
to.NetState.SendMessageLocalizedAffix(
Serial,
ItemID,
MessageType.Regular,
0x3B2,
3,
number,
"",
affixType,
affix,
args
);
}
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
{
ns.SendMessage(
Serial,
m_ItemID,
MessageType.Label,
0x3B2,
3,
false,
"ENU",
"",
$"{Name}{(m_Amount > 1 ? $" : {m_Amount}" : "")}"
);
}
}
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<Item> m_Items;
public string m_Name;
public int m_SavedFlags;
public ISpawner m_Spawner;
public int m_TempFlags;
public double m_Weight = -1;
}
[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
}
}