### Summary - `GenericEntityPersistence` is now a type of `GenericPersistence`. This allows developers to serialize both entities and non-entities in the same system. 🎉 - Each `SerializationThreadWorker` now allocates 1MB of heap for serialization _permanently_. If more memory is needed, that thread will double it's memory, not to exceed increments of 64MB. - Several bugs with serialization introduced with the pure MMF implementation have been fixed. - `BinaryFileReader` has been added back. 🎉 - Adds `world.useMultithreadedSaves` to allow disabling threaded saves. > [!IMPORTANT] > **Developer Note** > The split file serialization has been deprecated and is no longer used. We have effectively gone back to the same file writing we had before the pure MMF implementation.
4352 lines
114 KiB
C#
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 >= 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 != null)
|
|
{
|
|
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
|
|
}
|
|
}
|