## Summary Phase 3 of the anchored-time work: **every actively-written delta-time value in the engine now stores an anchored timestamp** — absolute on the wire, shifted forward by the downtime at load. Remaining time survives restarts (as delta did), and unlike delta, the bytes do not change on every save, so an idle world serializes identically save after save. The answer to "is it possible everywhere": **yes** — including the one case that looked impossible. ## The GenericPersistence problem, solved `GenericPersistence` bins (`Virtues.bin`, `StealableArtifacts.bin`, …) are raw payloads with no idx header, so they have no anchor of their own — anchored reads there would silently apply zero shift. But the anchor is a property of the **save**, not the file: every file in one save shares one `World.SaveStartTime`, and `Persistence.Load` reads **all** entity indexes (phase 1) before **any** persistence payload (phase 2). So the idx v5 header stamps a save-wide `World.LoadTimeShift`, and generic persistence readers inherit it. No file-format change, no per-bin header, old bins unaffected. ## Converted - **Item v10 → v11**: `LastMoved` — previously whole-minute delta, rewritten every save for every item, the single largest source of idle-save churn — and `DecayResetTime` (retiring the TODO from #2583). **Mobile v37 → v38**: the three stat-gain stamps. **BaseCreature v20 → v21**: `SummonEnd`. - **17 code-generated classes** (`[DeltaDateTime]` → `[AnchoredDateTime]`, version bump + `MigrateFrom` each): the five field spells, TransientItem, VirtueContext (×7 fields), PuzzleChestSolutionAndTime, BaseCamp, BaseBoat, RentedVendor, PlayerVendor, Ethics Player, Sheep, StarRoomGate, ChampionSpawn (×3), Corpse (`TimeOfDeath`, v19). The `MigrateFrom` bodies were generated from each class's current migration schema and are compiler-verified; VirtueContext's save-flagged nullables fall back to the same defaults the old deserialize left in place. Corpse's six migrations moved to a new `Corpse.Migrations.cs`. - **Hand-written sites**: StealableArtifacts (v2), VendorInventory (v1), ML quest objectives (persistence v3) — each gated on its own version. **Not converted, deliberately**: the ~25 read-only `ReadDeltaTime` sites in legacy version fallbacks and migration replays — they decode existing old bytes and must never change. `[DeltaDateTime]`/`WriteDeltaTime` remain available for them. ## Verification - Build 0 errors / 0 warnings; **837 + 708 tests green**. - Schema regeneration produced exactly the 17 expected new `vN.json` files (all `AnchoredTime` rule args), nothing else touched. - **New acceptance tests** pin the point of the whole effort: serializing the same item at two save times **5 hours apart produces byte-identical output**, and `LastMoved`/`DecayResetTime` round-trip **exactly** at sub-minute precision (the old minutes encoding destroyed both properties). ## Notes for review - `LastMoved` grows from a 1–3 byte encoded minutes value to 8-byte ticks per item — the price of byte-stability; it repays itself in incremental-save behavior since unchanged items now produce unchanged bytes. - BaseEscortable-style semantics are unchanged: anchored shift preserves *remaining* time exactly, the same contract delta provided, so no gameplay-visible behavior changes — deadlines simply stop being consumed by downtime that delta already protected against, now with stable bytes. ## Enforcement `WriteDeltaTime` is now `[Obsolete]` (interface + implementation). With the repo's warnings-as-errors, any new delta-time write — hand-written or emitted by a still-unconverted `[DeltaDateTime]` field — fails the build, with the migration instructions in the message. That the full solution still builds with **zero warnings** is itself the proof no active delta writer survived the conversion. `ReadDeltaTime` deliberately stays un-attributed: its remaining callers decode existing old bytes and are correct forever; its XML docs now state the legacy-decode-only contract.
4518 lines
120 KiB
C#
4518 lines
120 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: Item.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Diagnostics;
|
|
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();
|
|
|
|
var 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 partial 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;
|
|
|
|
// Where DecayScheduler tracks this item. Not serialized; PostDeserialize re-registers.
|
|
internal sbyte DecaySlot = DecayScheduler.SlotNone;
|
|
|
|
private int m_Hue;
|
|
private int m_ItemID;
|
|
private Layer m_Layer;
|
|
|
|
private Point3D m_Location;
|
|
private LootType m_LootType;
|
|
private Map m_Map;
|
|
private IEntity m_Parent; // Mobile, Item, or null=World
|
|
|
|
private ObjectPropertyList m_PropertyList;
|
|
|
|
[Constructible]
|
|
public Item(int itemID = 0)
|
|
{
|
|
m_ItemID = itemID;
|
|
Serial = World.NewItem;
|
|
|
|
// Assigned directly: the Visible/Movable setters and SetLastMoved() would register the item
|
|
// for decay while m_Map is still null, then unregister it once it is Map.Internal.
|
|
m_Flags = ImplFlag.Visible | ImplFlag.Movable;
|
|
Amount = 1;
|
|
m_Map = Map.Internal;
|
|
LastMoved = Core.Now;
|
|
|
|
World.AddEntity(this);
|
|
}
|
|
|
|
public Item(Serial serial) => Serial = serial;
|
|
|
|
public int TempFlags
|
|
{
|
|
get => LookupCompactInfo()?.m_TempFlags ?? 0;
|
|
set
|
|
{
|
|
var info = AcquireCompactInfo();
|
|
|
|
info.m_TempFlags = value;
|
|
|
|
if (info.m_TempFlags == 0)
|
|
{
|
|
VerifyCompactInfo();
|
|
}
|
|
}
|
|
}
|
|
|
|
public int SavedFlags
|
|
{
|
|
get => LookupCompactInfo()?.m_SavedFlags ?? 0;
|
|
set
|
|
{
|
|
var info = AcquireCompactInfo();
|
|
|
|
info.m_SavedFlags = value;
|
|
|
|
if (info.m_SavedFlags == 0)
|
|
{
|
|
VerifyCompactInfo();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sectors
|
|
[IgnoreDupe]
|
|
public Item Next { get; set; }
|
|
|
|
[IgnoreDupe]
|
|
public Item Previous { get; set; }
|
|
|
|
[IgnoreDupe]
|
|
public bool OnLinkList { get; set; }
|
|
|
|
/// <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 && Spawner == null;
|
|
|
|
private DateTime _lastMoved;
|
|
|
|
public DateTime LastMoved
|
|
{
|
|
get => _lastMoved;
|
|
set
|
|
{
|
|
_lastMoved = value;
|
|
|
|
// A move at or past the reset stamp supersedes it; drop it so the CompactInfo can collapse.
|
|
var info = LookupCompactInfo();
|
|
|
|
if (info != null && info.m_DecayReset != default && info.m_DecayReset <= value)
|
|
{
|
|
info.m_DecayReset = default;
|
|
VerifyCompactInfo();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Stackable
|
|
{
|
|
get => GetFlag(ImplFlag.Stackable);
|
|
set => SetFlag(ImplFlag.Stackable, value);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Visible
|
|
{
|
|
get => GetFlag(ImplFlag.Visible);
|
|
set
|
|
{
|
|
if (GetFlag(ImplFlag.Visible) != value)
|
|
{
|
|
SetFlag(ImplFlag.Visible, value);
|
|
|
|
if (m_Map != null)
|
|
{
|
|
var worldLoc = GetWorldLocation();
|
|
|
|
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (!m.CanSee(this) && m.InRange(worldLoc, GetUpdateRange(m)))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
Delta(ItemDelta.Update);
|
|
RestartDecay();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Movable
|
|
{
|
|
get => GetFlag(ImplFlag.Movable);
|
|
set
|
|
{
|
|
if (GetFlag(ImplFlag.Movable) != value)
|
|
{
|
|
SetFlag(ImplFlag.Movable, value);
|
|
|
|
Delta(ItemDelta.Update);
|
|
RestartDecay();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual bool ForceShowProperties => false;
|
|
|
|
public virtual bool HandlesOnMovement => false;
|
|
|
|
public static int LockedDownFlag { get; set; }
|
|
|
|
public static int SecureFlag { get; set; }
|
|
|
|
[IgnoreDupe]
|
|
public bool IsLockedDown
|
|
{
|
|
get => GetTempFlag(LockedDownFlag);
|
|
set
|
|
{
|
|
SetTempFlag(LockedDownFlag, value);
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
[IgnoreDupe]
|
|
public bool IsSecure
|
|
{
|
|
get => GetTempFlag(SecureFlag);
|
|
set
|
|
{
|
|
SetTempFlag(SecureFlag, value);
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
public virtual bool IsVirtualItem => false;
|
|
|
|
public virtual bool CanSeeStaffOnly(Mobile from) => from.AccessLevel > AccessLevel.Counselor;
|
|
|
|
public virtual int LabelNumber
|
|
{
|
|
get
|
|
{
|
|
if (m_ItemID < 0x4000)
|
|
{
|
|
return 1020000 + m_ItemID;
|
|
}
|
|
|
|
return 1078872 + m_ItemID;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalGold => GetTotal(TotalType.Gold);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalItems => GetTotal(TotalType.Items);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalWeight => GetTotal(TotalType.Weight);
|
|
|
|
public virtual double DefaultWeight
|
|
{
|
|
get
|
|
{
|
|
if (m_ItemID < 0 || m_ItemID > TileData.MaxItemValue || this is BaseMulti)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var weight = TileData.ItemTable[m_ItemID].Weight;
|
|
|
|
if (weight is 255 or 0)
|
|
{
|
|
weight = 1;
|
|
}
|
|
|
|
return weight;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public double Weight
|
|
{
|
|
get
|
|
{
|
|
var info = LookupCompactInfo();
|
|
|
|
return info is { m_Weight: >= 0 } ? info.m_Weight : DefaultWeight;
|
|
}
|
|
set
|
|
{
|
|
var defaultWeight = DefaultWeight;
|
|
var info = LookupCompactInfo();
|
|
var oldWeight = info is { m_Weight: >= 0 } ? info.m_Weight : defaultWeight;
|
|
|
|
if (oldWeight == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value < 0 || defaultWeight == value)
|
|
{
|
|
info?.m_Weight = -1;
|
|
VerifyCompactInfo();
|
|
}
|
|
else
|
|
{
|
|
(info ?? AcquireCompactInfo()).m_Weight = value;
|
|
}
|
|
|
|
UpdateTotal(this, TotalType.Weight, PileWeight - (int)Math.Ceiling(oldWeight * Amount));
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int PileWeight => (int)Math.Ceiling(Weight * Amount);
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Hue
|
|
{
|
|
get => m_Hue;
|
|
set
|
|
{
|
|
if (m_Hue != value)
|
|
{
|
|
m_Hue = value;
|
|
|
|
Delta(ItemDelta.Update);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual bool Nontransferable => QuestItem;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual Layer Layer
|
|
{
|
|
get => m_Layer;
|
|
set
|
|
{
|
|
if (m_Layer != value)
|
|
{
|
|
m_Layer = value;
|
|
|
|
Delta(ItemDelta.EquipOnly);
|
|
}
|
|
}
|
|
}
|
|
|
|
// SendMobileIncoming (0x78) sends only the first item per layer (it dedupes via the
|
|
// `layers` span). Equipment Layer comes from tiledata, so e.g. a two-handed weapon and a
|
|
// shield both resolve to Layer.TwoHanded; a later same-layer item sent on its own via
|
|
// EquipUpdate (0x2E) or its OPL would leave the client holding two items on one equipment
|
|
// slot (a use-after-free on the legacy 2D client when that slot is later torn down).
|
|
// Returns true if this equipped item is such a later duplicate and must not be sent alone.
|
|
public bool IsDupedEquipLayer() => m_Parent is Mobile m && m.FindItemOnLayer(m_Layer) != this;
|
|
|
|
public List<Item> Items => LookupItems() ?? EmptyItems;
|
|
|
|
public int LookupContainerVersion() => (this as Container)?._version ?? LookupCompactInfo()?.Version ?? 0;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public IEntity RootParent
|
|
{
|
|
get
|
|
{
|
|
var p = m_Parent;
|
|
|
|
while (p is Item item)
|
|
{
|
|
if (item.m_Parent == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
p = item.m_Parent;
|
|
}
|
|
|
|
return p;
|
|
}
|
|
}
|
|
|
|
public bool NoMoveHS { get; set; }
|
|
|
|
public virtual int PhysicalResistance => 0;
|
|
public virtual int FireResistance => 0;
|
|
public virtual int ColdResistance => 0;
|
|
public virtual int PoisonResistance => 0;
|
|
public virtual int EnergyResistance => 0;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int ItemID
|
|
{
|
|
get => m_ItemID;
|
|
set
|
|
{
|
|
if (m_ItemID != value)
|
|
{
|
|
var oldPileWeight = PileWeight;
|
|
|
|
m_ItemID = value;
|
|
|
|
var newPileWeight = PileWeight;
|
|
|
|
UpdateTotal(this, TotalType.Weight, newPileWeight - oldPileWeight);
|
|
|
|
InvalidateProperties();
|
|
Delta(ItemDelta.Update);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual string DefaultName => null;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string Name
|
|
{
|
|
get => LookupCompactInfo()?.m_Name ?? DefaultName;
|
|
set
|
|
{
|
|
var info = LookupCompactInfo();
|
|
|
|
if (value == null || value == DefaultName)
|
|
{
|
|
info?.m_Name = null;
|
|
VerifyCompactInfo();
|
|
}
|
|
else
|
|
{
|
|
(info ?? AcquireCompactInfo()).m_Name = value;
|
|
}
|
|
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
// Note: Setting the parent via command/props causes problems.
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
|
public IEntity Parent
|
|
{
|
|
get => m_Parent;
|
|
set
|
|
{
|
|
if (m_Parent == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldParent = m_Parent;
|
|
|
|
if (m_Map != null && oldParent == null && value != null)
|
|
{
|
|
m_Map.OnLeave(this);
|
|
}
|
|
|
|
m_Parent = value;
|
|
|
|
if (m_Map != null && oldParent != null && value == null)
|
|
{
|
|
m_Map.OnEnter(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public LightType Light
|
|
{
|
|
get => (LightType)m_Direction;
|
|
set
|
|
{
|
|
if ((LightType)m_Direction != value)
|
|
{
|
|
m_Direction = (Direction)value;
|
|
Delta(ItemDelta.Update);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Direction Direction
|
|
{
|
|
get => m_Direction;
|
|
set
|
|
{
|
|
if (m_Direction != value)
|
|
{
|
|
m_Direction = value;
|
|
Delta(ItemDelta.Update);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Amount
|
|
{
|
|
get => m_Amount;
|
|
set
|
|
{
|
|
var oldValue = m_Amount;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
var oldPileWeight = PileWeight;
|
|
|
|
m_Amount = value;
|
|
|
|
if (m_Amount <= 0)
|
|
{
|
|
logger.Error(
|
|
new Exception("Item.Amount <= 0 error"),
|
|
"Item {Type} ({Serial}) was changed to amount of {Amount}, but must be at least 1",
|
|
GetType(),
|
|
Serial,
|
|
m_Amount
|
|
);
|
|
}
|
|
|
|
var newPileWeight = PileWeight;
|
|
|
|
UpdateTotal(this, TotalType.Weight, newPileWeight - oldPileWeight);
|
|
|
|
OnAmountChange(oldValue);
|
|
|
|
Delta(ItemDelta.Update);
|
|
|
|
if (oldValue > 1 || value > 1)
|
|
{
|
|
InvalidateProperties();
|
|
}
|
|
|
|
if (!Stackable && m_Amount > 1)
|
|
{
|
|
logger.Warning(
|
|
"{Serial}: Amount changed for non-stackable item '{Name}'. ({Amount})",
|
|
Serial,
|
|
m_Amount,
|
|
GetType().Name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual bool HandlesOnSpeech => false;
|
|
|
|
public virtual bool BlocksFit => false;
|
|
|
|
public bool InSecureTrade => GetSecureTradeCont() != null;
|
|
|
|
public ItemData ItemData => TileData.ItemTable[m_ItemID & TileData.MaxItemValue];
|
|
|
|
public virtual bool CanTarget => true;
|
|
public virtual bool DisplayLootType => true;
|
|
|
|
public static bool ScissorCopyLootType { get; set; }
|
|
|
|
/// <summary>
|
|
/// True when the item was produced by the crafting system rather than bought or looted.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool PlayerConstructed { 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
|
|
{
|
|
get
|
|
{
|
|
if (m_PropertyList == null)
|
|
{
|
|
// Publish the list before building it so a nested InvalidateProperties can see the
|
|
// build in progress and defer instead of recursing into a second throwaway list.
|
|
var list = new ObjectPropertyList(this);
|
|
m_PropertyList = list;
|
|
InitializePropertyList(list);
|
|
}
|
|
|
|
return m_PropertyList;
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
AppendChildNameProperties(list);
|
|
}
|
|
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
|
public DateTime Created { get; set; } = Core.Now;
|
|
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public Serial Serial { get; }
|
|
|
|
public virtual void Serialize(IGenericWriter writer)
|
|
{
|
|
writer.Write(11); // 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.m_DecayReset > LastMoved)
|
|
{
|
|
flags |= SaveFlag.DecayReset;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (PlayerConstructed)
|
|
{
|
|
flags |= SaveFlag.PlayerConstructed;
|
|
}
|
|
|
|
writer.Write((int)flags);
|
|
|
|
// Anchored: shifted by downtime at load, so time-since-moved is preserved and the
|
|
// bytes are stable across saves while the item does not move.
|
|
writer.WriteAnchoredTime(LastMoved);
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.DecayReset))
|
|
{
|
|
writer.WriteAnchoredTime(info.m_DecayReset);
|
|
}
|
|
|
|
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;
|
|
|
|
// Register at the end instead: CanDecay() reads parent, map and location, none of which
|
|
// are final yet.
|
|
LastMoved = Core.Now;
|
|
|
|
if (Parent is Mobile mobile)
|
|
{
|
|
mobile.RemoveItem(this);
|
|
}
|
|
else if (Parent is Item item)
|
|
{
|
|
item.RemoveItem(this);
|
|
}
|
|
|
|
if (m_Map != map)
|
|
{
|
|
var old = m_Map;
|
|
|
|
if (m_Map != null)
|
|
{
|
|
m_Map.OnLeave(this);
|
|
|
|
if (oldLocation.m_X != 0)
|
|
{
|
|
SendRemovePacket(oldLocation);
|
|
}
|
|
}
|
|
|
|
m_Location = location;
|
|
OnLocationChange(oldRealLocation);
|
|
|
|
var items = LookupItems();
|
|
|
|
for (var i = 0; i < items.Count; ++i)
|
|
{
|
|
items[i].Map = map;
|
|
}
|
|
|
|
m_Map = map;
|
|
m_Map?.OnEnter(this);
|
|
|
|
OnMapChange();
|
|
|
|
if (m_Map != null)
|
|
{
|
|
var oldWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket();
|
|
var saWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket();
|
|
var hsWorldItem = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m)))
|
|
{
|
|
if (state.HighSeas)
|
|
{
|
|
var length = OutgoingEntityPackets.CreateWorldEntity(hsWorldItem, this, true);
|
|
if (length != hsWorldItem.Length)
|
|
{
|
|
hsWorldItem = hsWorldItem[..length];
|
|
}
|
|
|
|
SendInfoTo(state, hsWorldItem);
|
|
}
|
|
else if (state.StygianAbyss)
|
|
{
|
|
var length = OutgoingEntityPackets.CreateWorldEntity(saWorldItem, this, false);
|
|
if (length != saWorldItem.Length)
|
|
{
|
|
saWorldItem = saWorldItem[..length];
|
|
}
|
|
|
|
SendInfoTo(state, saWorldItem);
|
|
}
|
|
else
|
|
{
|
|
var length = OutgoingItemPackets.CreateWorldItem(oldWorldItem, this);
|
|
if (length != oldWorldItem.Length)
|
|
{
|
|
oldWorldItem = oldWorldItem[..length];
|
|
}
|
|
|
|
SendInfoTo(state, oldWorldItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RemDelta(ItemDelta.Update);
|
|
|
|
if (old == null || old == Map.Internal)
|
|
{
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
else if (m_Map != null)
|
|
{
|
|
if (oldLocation.m_X != 0)
|
|
{
|
|
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(oldLocation, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (!m.InRange(location, GetUpdateRange(m)))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
var oldInternalLocation = m_Location;
|
|
|
|
m_Location = location;
|
|
OnLocationChange(oldRealLocation);
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m)))
|
|
{
|
|
SendInfoTo(state);
|
|
}
|
|
}
|
|
|
|
m_Map.OnMove(oldInternalLocation, this);
|
|
|
|
RemDelta(ItemDelta.Update);
|
|
}
|
|
else
|
|
{
|
|
Map = map;
|
|
Location = location;
|
|
}
|
|
|
|
UpdateDecayRegistration();
|
|
}
|
|
|
|
/// <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 (m_Parent == null)
|
|
{
|
|
// A map change is a move; nothing else updates decay registration for a raw Map change.
|
|
SetLastMoved();
|
|
}
|
|
|
|
if (old == null || old == Map.Internal)
|
|
{
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void ProcessDelta()
|
|
{
|
|
var flags = m_DeltaFlags;
|
|
|
|
SetFlag(ImplFlag.InQueue, false);
|
|
m_DeltaFlags = ItemDelta.None;
|
|
|
|
var map = m_Map;
|
|
|
|
if (map == null || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var worldLoc = GetWorldLocation();
|
|
var update = (flags & ItemDelta.Update) != 0;
|
|
|
|
if (update && m_Parent is Container { IsPublicContainer: false } contParent)
|
|
{
|
|
var rootParent = contParent.RootParent as Mobile;
|
|
Mobile tradeRecip = null;
|
|
|
|
if (rootParent != null)
|
|
{
|
|
var ns = rootParent.NetState;
|
|
|
|
if (ns != null && rootParent.CanSee(this) && rootParent.InRange(worldLoc, GetUpdateRange(rootParent)))
|
|
{
|
|
ns.SendContainerContentUpdate(this);
|
|
SendOPLPacketTo(ns);
|
|
}
|
|
}
|
|
|
|
var st = GetSecureTradeCont()?.Trade;
|
|
|
|
if (st != null)
|
|
{
|
|
var test = st.From.Mobile;
|
|
|
|
if (test != null && test != rootParent)
|
|
{
|
|
tradeRecip = test;
|
|
}
|
|
|
|
test = st.To.Mobile;
|
|
|
|
if (test != null && test != rootParent)
|
|
{
|
|
tradeRecip = test;
|
|
}
|
|
|
|
var ns = tradeRecip?.NetState;
|
|
|
|
if (ns != null && tradeRecip.CanSee(this) && tradeRecip.InRange(worldLoc, GetUpdateRange(tradeRecip)))
|
|
{
|
|
ns.SendContainerContentUpdate(this);
|
|
SendOPLPacketTo(ns);
|
|
}
|
|
}
|
|
|
|
var openers = contParent.Openers;
|
|
|
|
if (openers != null)
|
|
{
|
|
for (var i = 0; i < openers.Count; ++i)
|
|
{
|
|
var mob = openers[i];
|
|
|
|
var range = GetUpdateRange(mob);
|
|
|
|
if (mob.Map != map || !mob.InRange(worldLoc, range))
|
|
{
|
|
openers.RemoveAt(i--);
|
|
}
|
|
else
|
|
{
|
|
if (mob == rootParent || mob == tradeRecip)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var ns = mob.NetState;
|
|
|
|
if (ns != null && mob.CanSee(this))
|
|
{
|
|
ns.SendContainerContentUpdate(this);
|
|
SendOPLPacketTo(ns);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (openers.Count == 0)
|
|
{
|
|
contParent.Openers = null;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// A second item sharing an equipment layer is omitted by SendMobileIncoming (0x78);
|
|
// sending it on its own via EquipUpdate/OPL would leave the client with two items on
|
|
// one slot. Skip the per-client sends entirely for the dupe.
|
|
if (IsDupedEquipLayer())
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var state in map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (!m.CanSee(this) || !m.InRange(worldLoc, GetUpdateRange(m)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (update)
|
|
{
|
|
if (m_Parent == null)
|
|
{
|
|
SendInfoTo(state);
|
|
}
|
|
else
|
|
{
|
|
if (m_Parent is Item)
|
|
{
|
|
// TODO: Optimize by writing once?
|
|
state.SendContainerContentUpdate(this);
|
|
}
|
|
else if (m_Parent is Mobile)
|
|
{
|
|
// TODO: Optimize by writing once?
|
|
state.SendEquipUpdate(this);
|
|
}
|
|
|
|
SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
else if ((flags & ItemDelta.EquipOnly) != 0 && m_Parent is Mobile)
|
|
{
|
|
state.SendEquipUpdate(this);
|
|
SendOPLPacketTo(state);
|
|
}
|
|
else if (ObjectPropertyList.Enabled && (flags & ItemDelta.Properties) != 0)
|
|
{
|
|
SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Delete()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnDelete();
|
|
DecayScheduler.Unregister(this);
|
|
|
|
var items = LookupItems();
|
|
|
|
for (var i = items.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i < items.Count)
|
|
{
|
|
items[i].OnParentDeleted(this);
|
|
}
|
|
}
|
|
|
|
SendRemovePacket();
|
|
|
|
SetFlag(ImplFlag.Deleted, true);
|
|
|
|
if (Parent is Mobile mobile)
|
|
{
|
|
mobile.RemoveItem(this);
|
|
}
|
|
else if (Parent is Item item)
|
|
{
|
|
item.RemoveItem(this);
|
|
}
|
|
|
|
ClearBounce();
|
|
|
|
if (m_Map != null)
|
|
{
|
|
if (m_Parent == null)
|
|
{
|
|
m_Map.OnLeave(this);
|
|
}
|
|
|
|
m_Map = null;
|
|
}
|
|
|
|
World.RemoveEntity(this);
|
|
|
|
OnAfterDelete();
|
|
ClearProperties();
|
|
}
|
|
|
|
public virtual bool SkipSerialization => false;
|
|
|
|
[IgnoreDupe]
|
|
public ISpawner Spawner
|
|
{
|
|
get => LookupCompactInfo()?.m_Spawner;
|
|
set
|
|
{
|
|
var info = AcquireCompactInfo();
|
|
var oldValue = info.m_Spawner;
|
|
|
|
info.m_Spawner = value;
|
|
|
|
if (info.m_Spawner == null)
|
|
{
|
|
VerifyCompactInfo();
|
|
}
|
|
|
|
if (oldValue != value)
|
|
{
|
|
RestartDecay();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void OnBeforeSpawn(Point3D location, Map m)
|
|
{
|
|
}
|
|
|
|
public virtual void OnAfterSpawn()
|
|
{
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public virtual Point3D Location
|
|
{
|
|
get => m_Location;
|
|
set
|
|
{
|
|
var oldLocation = m_Location;
|
|
|
|
if (oldLocation == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Map != null)
|
|
{
|
|
if (m_Parent == null)
|
|
{
|
|
if (m_Location.m_X != 0)
|
|
{
|
|
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(oldLocation, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (!m.InRange(value, GetUpdateRange(m)))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
var oldLoc = m_Location;
|
|
m_Location = value;
|
|
|
|
SetLastMoved();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m)) &&
|
|
(!state.HighSeas || !NoMoveHS || (m_DeltaFlags & ItemDelta.Update) != 0 ||
|
|
!m.InRange(oldLoc, GetUpdateRange(m))))
|
|
{
|
|
SendInfoTo(state);
|
|
}
|
|
}
|
|
|
|
RemDelta(ItemDelta.Update);
|
|
}
|
|
else
|
|
{
|
|
m_Location = value;
|
|
if (m_Parent is Item)
|
|
{
|
|
Delta(ItemDelta.Update);
|
|
}
|
|
}
|
|
|
|
if (m_Parent == null)
|
|
{
|
|
m_Map.OnMove(oldLocation, this);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_Location = value;
|
|
}
|
|
|
|
OnLocationChange(oldLocation);
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int X
|
|
{
|
|
get => Location.m_X;
|
|
set => Location = new Point3D(value, m_Location.m_Y, m_Location.m_Z);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int Y
|
|
{
|
|
get => Location.m_Y;
|
|
set => Location = new Point3D(m_Location.m_X, value, m_Location.m_Z);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int Z
|
|
{
|
|
get => Location.m_Z;
|
|
set => Location = new Point3D(m_Location.m_X, m_Location.m_Y, value);
|
|
}
|
|
|
|
public virtual bool InRange(Point2D p, int range) => Utility.InRange(p.X, p.Y, X, Y, range);
|
|
|
|
public virtual bool InRange(Point3D p, int range) => Utility.InRange(p.X, p.Y, X, Y, range);
|
|
|
|
public ExpandFlag GetExpandFlags()
|
|
{
|
|
var info = LookupCompactInfo();
|
|
|
|
ExpandFlag flags = 0;
|
|
|
|
if (info != null)
|
|
{
|
|
if (info.m_BlessedFor != null)
|
|
{
|
|
flags |= ExpandFlag.Blessed;
|
|
}
|
|
|
|
if (info.m_Bounce != null)
|
|
{
|
|
flags |= ExpandFlag.Bounce;
|
|
}
|
|
|
|
if (info.m_HeldBy != null)
|
|
{
|
|
flags |= ExpandFlag.Holder;
|
|
}
|
|
|
|
if (info.m_Items != null)
|
|
{
|
|
flags |= ExpandFlag.Items;
|
|
}
|
|
|
|
if (info.m_Name != null)
|
|
{
|
|
flags |= ExpandFlag.Name;
|
|
}
|
|
|
|
if (info.m_Spawner != null)
|
|
{
|
|
flags |= ExpandFlag.Spawner;
|
|
}
|
|
|
|
if (info.m_SavedFlags != 0)
|
|
{
|
|
flags |= ExpandFlag.SaveFlag;
|
|
}
|
|
|
|
if (info.m_TempFlags != 0)
|
|
{
|
|
flags |= ExpandFlag.TempFlag;
|
|
}
|
|
|
|
if (info.m_Weight >= 0)
|
|
{
|
|
flags |= ExpandFlag.Weight;
|
|
}
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
private CompactInfo LookupCompactInfo() => m_CompactInfo;
|
|
|
|
private CompactInfo AcquireCompactInfo() => m_CompactInfo ??= new CompactInfo();
|
|
|
|
private void ReleaseCompactInfo()
|
|
{
|
|
m_CompactInfo = null;
|
|
}
|
|
|
|
private void VerifyCompactInfo()
|
|
{
|
|
var info = m_CompactInfo;
|
|
|
|
if (info == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isValid = info.m_Name != null
|
|
|| info.m_Items != null
|
|
|| info.m_Bounce != null
|
|
|| info.m_HeldBy != null
|
|
|| info.m_BlessedFor != null
|
|
|| info.m_Spawner != null
|
|
|| info.m_DecayReset != default
|
|
|| info.m_TempFlags != 0
|
|
|| info.m_SavedFlags != 0
|
|
|| info.m_Weight >= 0;
|
|
|
|
if (!isValid)
|
|
{
|
|
ReleaseCompactInfo();
|
|
}
|
|
}
|
|
|
|
public List<Item> LookupItems() => (this is Container container ? container._items : LookupCompactInfo()?.m_Items) ?? EmptyItems;
|
|
|
|
public List<Item> AcquireItems()
|
|
{
|
|
if (this is Container cont)
|
|
{
|
|
return cont._items ??= new List<Item>();
|
|
}
|
|
|
|
var info = AcquireCompactInfo();
|
|
return 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 (ServerFeatureFlags.InsuranceEnabled && 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);
|
|
}
|
|
}
|
|
|
|
/// <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.Murderer)
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
if (parent.Player && Nontransferable)
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
return DeathMoveResult.MoveToCorpse;
|
|
}
|
|
|
|
public virtual DeathMoveResult OnInventoryDeath(Mobile parent)
|
|
{
|
|
if (!Movable)
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
if (parent.KeepsItemsOnDeath)
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
if (CheckBlessed(parent))
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
if (CheckNewbied() && !parent.Murderer)
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
if (parent.Player && Nontransferable)
|
|
{
|
|
return DeathMoveResult.MoveToBackpack;
|
|
}
|
|
|
|
return DeathMoveResult.MoveToCorpse;
|
|
}
|
|
|
|
/// <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 DateTime ScheduledDecayTime
|
|
{
|
|
get
|
|
{
|
|
var reset = DecayResetTime;
|
|
var lastMoved = LastMoved;
|
|
|
|
return (reset > lastMoved ? reset : lastMoved) + DecayTime;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When decay eligibility was last restored without the item moving, e.g. a GM unfreezing it.
|
|
/// The decay countdown runs from the later of this and <see cref="LastMoved" />.
|
|
/// </summary>
|
|
public DateTime DecayResetTime
|
|
{
|
|
get => LookupCompactInfo()?.m_DecayReset ?? default;
|
|
private set
|
|
{
|
|
if (value == default)
|
|
{
|
|
var info = LookupCompactInfo();
|
|
|
|
if (info != null && info.m_DecayReset != default)
|
|
{
|
|
info.m_DecayReset = default;
|
|
VerifyCompactInfo();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AcquireCompactInfo().m_DecayReset = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restarts the decay countdown without touching <see cref="LastMoved" />: call when decay
|
|
/// eligibility changes state (Movable/Visible/Spawner) or a region refuses a decay, where a
|
|
/// stale <see cref="LastMoved" /> would otherwise decay the item on the next tick.
|
|
/// Stamps <see cref="DecayResetTime" /> only when that extends the current deadline, then
|
|
/// updates the scheduler registration.
|
|
/// </summary>
|
|
public void RestartDecay()
|
|
{
|
|
if (CanDecay())
|
|
{
|
|
var now = Core.Now;
|
|
|
|
if (ScheduledDecayTime < now + DecayTime)
|
|
{
|
|
DecayResetTime = now;
|
|
}
|
|
}
|
|
|
|
UpdateDecayRegistration();
|
|
}
|
|
|
|
public void UpdateDecayRegistration()
|
|
{
|
|
DecayScheduler.Unregister(this);
|
|
|
|
if (CanDecay())
|
|
{
|
|
DecayScheduler.Register(this);
|
|
}
|
|
else
|
|
{
|
|
// No countdown to anchor while ineligible; drop the stamp so the CompactInfo
|
|
// can collapse. Re-eligibility always re-anchors.
|
|
DecayResetTime = default;
|
|
}
|
|
}
|
|
|
|
public void SetLastMoved()
|
|
{
|
|
LastMoved = Core.Now;
|
|
UpdateDecayRegistration();
|
|
}
|
|
|
|
public virtual bool CanStackWith(Item dropped) =>
|
|
dropped.Stackable && Stackable &&
|
|
dropped.GetType() == GetType() &&
|
|
dropped.ItemID == ItemID &&
|
|
dropped.Hue == Hue &&
|
|
dropped.Name == Name &&
|
|
dropped.Amount + Amount <= 60000 &&
|
|
dropped != this;
|
|
|
|
public bool StackWith(Mobile from, Item dropped) => StackWith(from, dropped, true);
|
|
|
|
public virtual bool StackWith(Mobile from, Item dropped, bool playSound)
|
|
{
|
|
if (!CanStackWith(dropped))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m_LootType != dropped.m_LootType)
|
|
{
|
|
m_LootType = LootType.Regular;
|
|
}
|
|
|
|
Amount += dropped.Amount;
|
|
if (PlayerConstructed != dropped.PlayerConstructed)
|
|
{
|
|
PlayerConstructed = false;
|
|
}
|
|
|
|
dropped.Delete();
|
|
|
|
if (playSound && from != null)
|
|
{
|
|
var soundID = GetDropSound();
|
|
|
|
if (soundID == -1)
|
|
{
|
|
soundID = 0x42;
|
|
}
|
|
|
|
from.SendSound(soundID, GetWorldLocation());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool OnDragDrop(Mobile from, Item dropped)
|
|
{
|
|
var success = (Parent as Container)?.OnStackAttempt(from, this, dropped) ??
|
|
StackWith(from, dropped);
|
|
|
|
if (success && Spawner != null)
|
|
{
|
|
Spawner.Remove(this);
|
|
Spawner = null;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
public Rectangle2D GetGraphicBounds()
|
|
{
|
|
var itemID = m_ItemID;
|
|
var doubled = m_Amount > 1;
|
|
|
|
if (itemID is >= 0xEEA and <= 0xEF2) // Are we coins?
|
|
{
|
|
var coinBase = (itemID - 0xEEA) / 3;
|
|
coinBase *= 3;
|
|
coinBase += 0xEEA;
|
|
|
|
doubled = false;
|
|
|
|
itemID = m_Amount switch
|
|
{
|
|
<= 1 => coinBase,
|
|
<= 5 => coinBase + 1,
|
|
_ => coinBase + 2
|
|
};
|
|
}
|
|
|
|
var bounds = ItemBounds.Bounds[itemID & 0x3FFF];
|
|
|
|
if (doubled)
|
|
{
|
|
bounds.Set(bounds.X, bounds.Y, bounds.Width + 5, bounds.Height + 5);
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
|
|
public virtual void AppendChildProperties(IPropertyList list)
|
|
{
|
|
if (m_Parent is Item item)
|
|
{
|
|
item.GetChildProperties(list, this);
|
|
}
|
|
else if (m_Parent is Mobile mobile)
|
|
{
|
|
mobile.GetChildProperties(list, this);
|
|
}
|
|
}
|
|
|
|
public virtual void AppendChildNameProperties(IPropertyList list)
|
|
{
|
|
if (m_Parent is Item item)
|
|
{
|
|
item.GetChildNameProperties(list, this);
|
|
}
|
|
else if (m_Parent is Mobile mobile)
|
|
{
|
|
mobile.GetChildNameProperties(list, this);
|
|
}
|
|
}
|
|
|
|
private ObjectPropertyList InitializePropertyList(ObjectPropertyList list)
|
|
{
|
|
list.IsBuilding = true;
|
|
|
|
try
|
|
{
|
|
GetProperties(list);
|
|
AppendChildProperties(list);
|
|
list.Terminate();
|
|
}
|
|
finally
|
|
{
|
|
list.IsBuilding = false;
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
public virtual void ClearProperties()
|
|
{
|
|
m_PropertyList = null;
|
|
}
|
|
|
|
#nullable enable
|
|
public virtual void InvalidateProperties()
|
|
{
|
|
if (!ObjectPropertyList.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Always a bug in the property getter, and there is no correct recovery: refuse rather than
|
|
// hide it. RELEASE keeps a possibly stale tooltip, DEBUG throws.
|
|
// See dev-docs/property-lists.md "Never Invalidate From Inside GetProperties".
|
|
if (m_PropertyList?.IsBuilding == true)
|
|
{
|
|
logger.Error(
|
|
"{Entity} called InvalidateProperties() while its property list was being built. Remove the side effect from the property getter, or defer it with Timer.DelayCall.\n{StackTrace}",
|
|
this,
|
|
new StackTrace()
|
|
);
|
|
|
|
#if DEBUG
|
|
throw new InvalidOperationException(
|
|
$"{this} invalidated its property list from inside GetProperties. Remove the side effect from the property getter."
|
|
);
|
|
#else
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
if (m_Map != null && m_Map != Map.Internal && !World.Loading)
|
|
{
|
|
int? oldHash;
|
|
int newHash;
|
|
if (m_PropertyList != null)
|
|
{
|
|
oldHash = m_PropertyList.Hash;
|
|
m_PropertyList.Reset();
|
|
InitializePropertyList(m_PropertyList);
|
|
newHash = m_PropertyList.Hash;
|
|
}
|
|
else
|
|
{
|
|
oldHash = null;
|
|
newHash = PropertyList.Hash;
|
|
}
|
|
|
|
if (oldHash != newHash)
|
|
{
|
|
Delta(ItemDelta.Properties);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearProperties();
|
|
}
|
|
}
|
|
#nullable restore
|
|
|
|
public virtual int GetPacketFlags()
|
|
{
|
|
var flags = 0;
|
|
|
|
if (!Visible)
|
|
{
|
|
flags |= 0x80;
|
|
}
|
|
|
|
if (Movable || ForceShowProperties)
|
|
{
|
|
flags |= 0x20;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
public virtual bool OnMoveOff(Mobile m) => true;
|
|
|
|
public virtual bool OnMoveOver(Mobile m) => true;
|
|
|
|
public virtual void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
}
|
|
|
|
public void Internalize()
|
|
{
|
|
MoveToWorld(Point3D.Zero, Map.Internal);
|
|
}
|
|
|
|
public virtual void OnMapChange()
|
|
{
|
|
}
|
|
|
|
public virtual void OnRemoved(IEntity parent)
|
|
{
|
|
}
|
|
|
|
public virtual void OnAdded(IEntity parent)
|
|
{
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool GetSaveFlag(SaveFlag flags, SaveFlag toGet) => (flags & toGet) != 0;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ItemAtEnumerable<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();
|
|
|
|
// Default for versions without a saved value. Stamp only: map and parent are still unread,
|
|
// and PostDeserialize registers every item once its state is final.
|
|
LastMoved = Core.Now;
|
|
|
|
switch (version)
|
|
{
|
|
case 11:
|
|
case 10:
|
|
case 9:
|
|
case 8:
|
|
case 7:
|
|
case 6:
|
|
{
|
|
var flags = (SaveFlag)reader.ReadInt();
|
|
|
|
if (version >= 11)
|
|
{
|
|
LastMoved = reader.ReadAnchoredTime();
|
|
}
|
|
else if (version < 7)
|
|
{
|
|
LastMoved = reader.ReadDeltaTime();
|
|
}
|
|
else
|
|
{
|
|
var minutes = reader.ReadEncodedInt();
|
|
|
|
try
|
|
{
|
|
LastMoved = Core.Now - TimeSpan.FromMinutes(minutes);
|
|
}
|
|
catch
|
|
{
|
|
LastMoved = Core.Now;
|
|
}
|
|
}
|
|
|
|
if (version >= 10 && GetSaveFlag(flags, SaveFlag.DecayReset))
|
|
{
|
|
var reset = version >= 11 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime();
|
|
|
|
// Pre-v11 LastMoved was stored at whole-minute precision; keep the
|
|
// stamp only while it still extends the deadline.
|
|
if (reset > LastMoved)
|
|
{
|
|
DecayResetTime = reset;
|
|
}
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Direction))
|
|
{
|
|
m_Direction = (Direction)reader.ReadByte();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Bounce))
|
|
{
|
|
AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader);
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.LootType))
|
|
{
|
|
m_LootType = (LootType)reader.ReadByte();
|
|
}
|
|
|
|
int x = 0, y = 0, z = 0;
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.LocationFull))
|
|
{
|
|
x = reader.ReadEncodedInt();
|
|
y = reader.ReadEncodedInt();
|
|
z = reader.ReadEncodedInt();
|
|
}
|
|
else
|
|
{
|
|
if (GetSaveFlag(flags, SaveFlag.LocationByteXY))
|
|
{
|
|
x = reader.ReadByte();
|
|
y = reader.ReadByte();
|
|
}
|
|
else if (GetSaveFlag(flags, SaveFlag.LocationShortXY))
|
|
{
|
|
x = reader.ReadShort();
|
|
y = reader.ReadShort();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.LocationSByteZ))
|
|
{
|
|
z = reader.ReadSByte();
|
|
}
|
|
}
|
|
|
|
m_Location = new Point3D(x, y, z);
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.ItemID))
|
|
{
|
|
m_ItemID = reader.ReadEncodedInt();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Hue))
|
|
{
|
|
m_Hue = reader.ReadEncodedInt();
|
|
}
|
|
|
|
m_Amount = GetSaveFlag(flags, SaveFlag.Amount) ? reader.ReadEncodedInt() : 1;
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Layer))
|
|
{
|
|
m_Layer = (Layer)reader.ReadByte();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Name))
|
|
{
|
|
var name = reader.ReadString();
|
|
|
|
if (name != DefaultName)
|
|
{
|
|
AcquireCompactInfo().m_Name = name;
|
|
}
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Parent))
|
|
{
|
|
var parent = reader.ReadSerial();
|
|
|
|
if (parent.IsMobile)
|
|
{
|
|
m_Parent = World.FindMobile(parent);
|
|
}
|
|
else if (parent.IsItem)
|
|
{
|
|
m_Parent = World.FindItem(parent);
|
|
}
|
|
else
|
|
{
|
|
m_Parent = null;
|
|
}
|
|
|
|
if (m_Parent == null && (parent.IsMobile || parent.IsItem))
|
|
{
|
|
Timer.DelayCall(Delete);
|
|
}
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Items))
|
|
{
|
|
var items = reader.ReadEntityList<Item>();
|
|
|
|
if (this is Container)
|
|
{
|
|
(this as Container)._items = items;
|
|
}
|
|
else
|
|
{
|
|
AcquireCompactInfo().m_Items = items;
|
|
}
|
|
}
|
|
|
|
if (version < 8 || !GetSaveFlag(flags, SaveFlag.NullWeight))
|
|
{
|
|
double weight;
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.IntWeight))
|
|
{
|
|
weight = reader.ReadEncodedInt();
|
|
}
|
|
else if (GetSaveFlag(flags, SaveFlag.WeightNot1or0))
|
|
{
|
|
weight = reader.ReadDouble();
|
|
}
|
|
else if (GetSaveFlag(flags, SaveFlag.WeightIs0))
|
|
{
|
|
weight = 0.0;
|
|
}
|
|
else
|
|
{
|
|
weight = 1.0;
|
|
}
|
|
|
|
if (weight != DefaultWeight)
|
|
{
|
|
AcquireCompactInfo().m_Weight = weight;
|
|
}
|
|
}
|
|
|
|
m_Map = GetSaveFlag(flags, SaveFlag.Map) ? reader.ReadMap() : Map.Internal;
|
|
|
|
SetFlag(ImplFlag.Visible, !GetSaveFlag(flags, SaveFlag.Visible) || reader.ReadBool());
|
|
|
|
SetFlag(ImplFlag.Movable, !GetSaveFlag(flags, SaveFlag.Movable) || reader.ReadBool());
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Stackable))
|
|
{
|
|
SetFlag(ImplFlag.Stackable, reader.ReadBool());
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.ImplFlags))
|
|
{
|
|
m_Flags = (ImplFlag)reader.ReadEncodedInt();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.InsuredFor))
|
|
/*m_InsuredFor = */
|
|
{
|
|
reader.ReadEntity<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();
|
|
}
|
|
|
|
PlayerConstructed = GetSaveFlag(flags, SaveFlag.PlayerConstructed);
|
|
|
|
if (m_Map != null && m_Parent == null)
|
|
{
|
|
m_Map.OnEnter(this);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 5:
|
|
{
|
|
var flags = (SaveFlag)reader.ReadInt();
|
|
|
|
LastMoved = reader.ReadDeltaTime();
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Direction))
|
|
{
|
|
m_Direction = (Direction)reader.ReadByte();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Bounce))
|
|
{
|
|
AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader);
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.LootType))
|
|
{
|
|
m_LootType = (LootType)reader.ReadByte();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.LocationFull))
|
|
{
|
|
m_Location = reader.ReadPoint3D();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.ItemID))
|
|
{
|
|
m_ItemID = reader.ReadInt();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Hue))
|
|
{
|
|
m_Hue = reader.ReadInt();
|
|
}
|
|
|
|
m_Amount = GetSaveFlag(flags, SaveFlag.Amount) ? reader.ReadInt() : 1;
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Layer))
|
|
{
|
|
m_Layer = (Layer)reader.ReadByte();
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Name))
|
|
{
|
|
var name = reader.ReadString();
|
|
|
|
if (name != DefaultName)
|
|
{
|
|
AcquireCompactInfo().m_Name = name;
|
|
}
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Parent))
|
|
{
|
|
var parent = reader.ReadSerial();
|
|
|
|
if (parent.IsMobile)
|
|
{
|
|
m_Parent = World.FindMobile(parent);
|
|
}
|
|
else if (parent.IsItem)
|
|
{
|
|
m_Parent = World.FindItem(parent);
|
|
}
|
|
else
|
|
{
|
|
m_Parent = null;
|
|
}
|
|
|
|
if (m_Parent == null && (parent.IsMobile || parent.IsItem))
|
|
{
|
|
Timer.DelayCall(Delete);
|
|
}
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Items))
|
|
{
|
|
var items = reader.ReadEntityList<Item>();
|
|
|
|
if (this is Container cont)
|
|
{
|
|
cont._items = items;
|
|
}
|
|
else
|
|
{
|
|
AcquireCompactInfo().m_Items = items;
|
|
}
|
|
}
|
|
|
|
double weight;
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.IntWeight))
|
|
{
|
|
weight = reader.ReadEncodedInt();
|
|
}
|
|
else if (GetSaveFlag(flags, SaveFlag.WeightNot1or0))
|
|
{
|
|
weight = reader.ReadDouble();
|
|
}
|
|
else if (GetSaveFlag(flags, SaveFlag.WeightIs0))
|
|
{
|
|
weight = 0.0;
|
|
}
|
|
else
|
|
{
|
|
weight = 1.0;
|
|
}
|
|
|
|
if (weight != DefaultWeight)
|
|
{
|
|
AcquireCompactInfo().m_Weight = weight;
|
|
}
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Map))
|
|
{
|
|
m_Map = reader.ReadMap();
|
|
}
|
|
else
|
|
{
|
|
m_Map = Map.Internal;
|
|
}
|
|
|
|
SetFlag(ImplFlag.Visible, !GetSaveFlag(flags, SaveFlag.Visible) || reader.ReadBool());
|
|
SetFlag(ImplFlag.Movable, !GetSaveFlag(flags, SaveFlag.Movable) || reader.ReadBool());
|
|
|
|
if (GetSaveFlag(flags, SaveFlag.Stackable))
|
|
{
|
|
SetFlag(ImplFlag.Stackable, reader.ReadBool());
|
|
}
|
|
|
|
if (m_Map != null && m_Parent == null)
|
|
{
|
|
m_Map.OnEnter(this);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 4: // Just removed variables
|
|
case 3:
|
|
{
|
|
m_Direction = (Direction)reader.ReadInt();
|
|
|
|
goto case 2;
|
|
}
|
|
case 2:
|
|
{
|
|
AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader);
|
|
LastMoved = reader.ReadDeltaTime();
|
|
|
|
goto case 1;
|
|
}
|
|
case 1:
|
|
{
|
|
m_LootType = (LootType)reader.ReadByte(); // m_Newbied = reader.ReadBool();
|
|
|
|
goto case 0;
|
|
}
|
|
case 0:
|
|
{
|
|
m_Location = reader.ReadPoint3D();
|
|
m_ItemID = reader.ReadInt();
|
|
m_Hue = reader.ReadInt();
|
|
m_Amount = reader.ReadInt();
|
|
m_Layer = (Layer)reader.ReadByte();
|
|
|
|
var name = reader.ReadString();
|
|
|
|
if (name != DefaultName)
|
|
{
|
|
AcquireCompactInfo().m_Name = name;
|
|
}
|
|
|
|
var parent = reader.ReadSerial();
|
|
|
|
if (parent.IsMobile)
|
|
{
|
|
m_Parent = World.FindMobile(parent);
|
|
}
|
|
else if (parent.IsItem)
|
|
{
|
|
m_Parent = World.FindItem(parent);
|
|
}
|
|
else
|
|
{
|
|
m_Parent = null;
|
|
}
|
|
|
|
if (m_Parent == null && (parent.IsMobile || parent.IsItem))
|
|
{
|
|
Timer.DelayCall(Delete);
|
|
}
|
|
|
|
var count = reader.ReadInt();
|
|
|
|
if (count > 0)
|
|
{
|
|
var items = new List<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._items = items;
|
|
}
|
|
else
|
|
{
|
|
AcquireCompactInfo().m_Items = items;
|
|
}
|
|
}
|
|
|
|
var weight = reader.ReadDouble();
|
|
|
|
if (weight != DefaultWeight)
|
|
{
|
|
AcquireCompactInfo().m_Weight = weight;
|
|
}
|
|
|
|
if (version <= 3)
|
|
{
|
|
reader.ReadInt();
|
|
reader.ReadInt();
|
|
reader.ReadInt();
|
|
}
|
|
|
|
m_Map = reader.ReadMap();
|
|
SetFlag(ImplFlag.Visible, reader.ReadBool());
|
|
SetFlag(ImplFlag.Movable, reader.ReadBool());
|
|
|
|
if (version <= 3)
|
|
/*m_Deleted =*/
|
|
{
|
|
reader.ReadBool();
|
|
}
|
|
|
|
Stackable = reader.ReadBool();
|
|
|
|
if (m_Map != null && m_Parent == null)
|
|
{
|
|
m_Map.OnEnter(this);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (HeldBy != null)
|
|
{
|
|
Timer.StartTimer(FixHolding_Sandbox);
|
|
}
|
|
|
|
// if (version < 9)
|
|
VerifyCompactInfo();
|
|
}
|
|
|
|
private void FixHolding_Sandbox()
|
|
{
|
|
var heldBy = HeldBy;
|
|
|
|
if (heldBy != null)
|
|
{
|
|
if (GetBounce() != null)
|
|
{
|
|
Bounce(heldBy);
|
|
}
|
|
else
|
|
{
|
|
heldBy.Holding = null;
|
|
heldBy.AddToBackpack(this);
|
|
ClearBounce();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int GetMaxUpdateRange() => 18;
|
|
|
|
public virtual int GetUpdateRange(Mobile m) => 18;
|
|
|
|
public virtual void SendInfoTo(NetState ns, ReadOnlySpan<byte> world = default)
|
|
{
|
|
SendWorldPacketTo(ns, world);
|
|
SendOPLPacketTo(ns);
|
|
}
|
|
|
|
public virtual void SendOPLPacketTo(NetState ns)
|
|
{
|
|
if (ObjectPropertyList.Enabled)
|
|
{
|
|
ns.SendOPLInfo(this);
|
|
}
|
|
}
|
|
|
|
public virtual void SendWorldPacketTo(NetState ns, ReadOnlySpan<byte> world = default)
|
|
{
|
|
if (world != ReadOnlySpan<byte>.Empty)
|
|
{
|
|
ns?.Send(world);
|
|
return;
|
|
}
|
|
|
|
ns.SendWorldItem(this);
|
|
}
|
|
|
|
public virtual int GetTotal(TotalType type) => 0;
|
|
|
|
public virtual void UpdateTotal(Item sender, TotalType type, int delta)
|
|
{
|
|
if (!IsVirtualItem)
|
|
{
|
|
if (m_Parent is Item item)
|
|
{
|
|
item.UpdateTotal(sender, type, delta);
|
|
}
|
|
else if (m_Parent is Mobile mobile)
|
|
{
|
|
mobile.UpdateTotal(sender, type, delta);
|
|
}
|
|
else
|
|
{
|
|
HeldBy?.UpdateTotal(sender, type, delta);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void UpdateTotals()
|
|
{
|
|
}
|
|
|
|
public virtual void HandleInvalidTransfer(Mobile from)
|
|
{
|
|
// OSI sends 1074769, bug!
|
|
if (QuestItem)
|
|
{
|
|
// You can only drop quest items into the top-most level of your backpack
|
|
// while you still need them for your quest.
|
|
from.SendLocalizedMessage(1049343);
|
|
}
|
|
}
|
|
|
|
public bool ParentsContain<T>() where T : Item
|
|
{
|
|
var p = m_Parent;
|
|
|
|
while (p is Item item)
|
|
{
|
|
if (item is T)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (item.m_Parent == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
p = item.m_Parent;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual void AddItem(Item item)
|
|
{
|
|
if (item?.Deleted != false || item.m_Parent == this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item == this)
|
|
{
|
|
var customException = new InvalidOperationException("Adding item to itself");
|
|
logger.Warning(
|
|
customException,
|
|
"Adding item to itself: ({Serial1} {Item1}).AddItem({Serial2} {Item2})",
|
|
Serial,
|
|
GetType().Name,
|
|
item.Serial,
|
|
item.GetType().Name
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (IsChildOf(item))
|
|
{
|
|
var customException = new InvalidOperationException("Adding parent item to child");
|
|
logger.Warning(
|
|
customException,
|
|
"Adding parent item to child: [{Serial1} {Item1}].AddItem( [{Serial2} {Item2}] )",
|
|
Serial,
|
|
GetType().Name,
|
|
item.Serial,
|
|
item.GetType().Name
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (item.m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.RemoveItem(item);
|
|
}
|
|
else if (item.m_Parent is Item parentItem)
|
|
{
|
|
parentItem.RemoveItem(item);
|
|
}
|
|
else
|
|
{
|
|
item.SendRemovePacket();
|
|
}
|
|
|
|
item.Parent = this;
|
|
item.Map = m_Map;
|
|
|
|
var items = AcquireItems();
|
|
items.Add(item);
|
|
|
|
if (this is not Container)
|
|
{
|
|
AcquireCompactInfo().Version++;
|
|
}
|
|
|
|
if (!item.IsVirtualItem)
|
|
{
|
|
UpdateTotal(item, TotalType.Gold, item.TotalGold);
|
|
UpdateTotal(item, TotalType.Items, item.TotalItems + 1);
|
|
UpdateTotal(item, TotalType.Weight, item.TotalWeight + item.PileWeight);
|
|
}
|
|
|
|
item.Delta(ItemDelta.Update);
|
|
item.UpdateDecayRegistration();
|
|
|
|
item.OnAdded(this);
|
|
OnItemAdded(item);
|
|
}
|
|
|
|
public void Delta(ItemDelta flags)
|
|
{
|
|
if (m_Map == null || m_Map == Map.Internal)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_DeltaFlags |= flags;
|
|
|
|
if (!GetFlag(ImplFlag.InQueue))
|
|
{
|
|
SetFlag(ImplFlag.InQueue, true);
|
|
m_DeltaQueue.Enqueue(this);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void RemDelta(ItemDelta flags)
|
|
{
|
|
m_DeltaFlags &= ~flags;
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when deltas remain queued after a <see cref="ProcessDeltaQueue"/> pass, which is
|
|
/// bounded by the count it saw on entry. The event loop consults this before sleeping.
|
|
/// </summary>
|
|
public static bool HasQueuedDeltas => m_DeltaQueue.Count > 0;
|
|
|
|
public static void ProcessDeltaQueue()
|
|
{
|
|
var limit = m_DeltaQueue.Count;
|
|
|
|
while (m_DeltaQueue.Count > 0 && --limit >= 0)
|
|
{
|
|
var item = m_DeltaQueue.Dequeue();
|
|
|
|
if (item == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
item.SetFlag(ImplFlag.InQueue, false);
|
|
|
|
try
|
|
{
|
|
item.ProcessDelta();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Debug(ex, "Process Delta Queue for {Item} failed", item);
|
|
}
|
|
}
|
|
|
|
if (m_DeltaQueue.Count > 0)
|
|
{
|
|
logger.Warning(
|
|
"{Count} items left in delta queue after processing.",
|
|
m_DeltaQueue.Count
|
|
);
|
|
}
|
|
}
|
|
|
|
#if TRACK_LEAKS
|
|
~Item()
|
|
{
|
|
EntityFinalizationTracker.NotifyFinalized(this);
|
|
}
|
|
#endif
|
|
|
|
public virtual void OnDelete()
|
|
{
|
|
if (Spawner != null)
|
|
{
|
|
Spawner.Remove(this);
|
|
Spawner = null;
|
|
}
|
|
}
|
|
|
|
public virtual void OnParentDeleted(IEntity parent)
|
|
{
|
|
Delete();
|
|
}
|
|
|
|
public virtual void OnAfterDelete()
|
|
{
|
|
}
|
|
|
|
public virtual void RemoveItem(Item item)
|
|
{
|
|
var items = LookupItems();
|
|
|
|
if (items.Remove(item))
|
|
{
|
|
if (this is not Container)
|
|
{
|
|
AcquireCompactInfo().Version++;
|
|
}
|
|
|
|
item.SendRemovePacket();
|
|
|
|
if (!item.IsVirtualItem)
|
|
{
|
|
UpdateTotal(item, TotalType.Gold, -item.TotalGold);
|
|
UpdateTotal(item, TotalType.Items, -(item.TotalItems + 1));
|
|
UpdateTotal(item, TotalType.Weight, -(item.TotalWeight + item.PileWeight));
|
|
}
|
|
|
|
item.Parent = null;
|
|
item.UpdateDecayRegistration();
|
|
|
|
item.OnRemoved(this);
|
|
OnItemRemoved(item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnAfterDuped(Item newItem)
|
|
{
|
|
}
|
|
|
|
// Warning: This uses reflection and is slow!
|
|
public virtual void Dupe(Item newItem)
|
|
{
|
|
CopyProperties(this, newItem);
|
|
OnAfterDuped(newItem);
|
|
}
|
|
|
|
// Warning: This uses reflection and is slow!
|
|
public static void CopyProperties(Item src, Item dest)
|
|
{
|
|
var props = src.GetType().GetProperties();
|
|
|
|
for (var i = 0; i < props.Length; i++)
|
|
{
|
|
var p = props[i];
|
|
if (p.GetCustomAttribute<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;
|
|
}
|
|
|
|
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (m.InRange(worldLoc, GetUpdateRange(m)))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int GetDropSound() => -1;
|
|
|
|
public Point3D GetWorldLocation()
|
|
{
|
|
var root = RootParent;
|
|
|
|
if (root == null)
|
|
{
|
|
return m_Location;
|
|
}
|
|
|
|
return root.Location;
|
|
|
|
// return root == null ? m_Location : new Point3D( (IPoint3D) root );
|
|
}
|
|
|
|
public IPoint3D GetSurfaceTop()
|
|
{
|
|
var root = RootParent;
|
|
|
|
return (root as Item)?.GetSurfaceTop() ?? (ItemData.Surface ? new Point3D(
|
|
m_Location.m_X,
|
|
m_Location.m_Y,
|
|
m_Location.m_Z + ItemData.CalcHeight
|
|
) : this);
|
|
}
|
|
|
|
public Point3D GetWorldTop()
|
|
{
|
|
var root = RootParent;
|
|
|
|
return (root as Item)?.GetWorldTop() ?? new Point3D(
|
|
m_Location.m_X,
|
|
m_Location.m_Y,
|
|
m_Location.m_Z + ItemData.CalcHeight
|
|
);
|
|
}
|
|
|
|
public virtual void OnSnoop(Mobile from)
|
|
{
|
|
}
|
|
|
|
public SecureTradeContainer GetSecureTradeCont()
|
|
{
|
|
object p = this;
|
|
|
|
while (p is Item item)
|
|
{
|
|
if (item is SecureTradeContainer container)
|
|
{
|
|
return container;
|
|
}
|
|
|
|
p = item.m_Parent;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual void OnItemAdded(Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSubItemAdded(item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnSubItemAdded(item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnItemRemoved(Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSubItemRemoved(item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnSubItemRemoved(item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnSubItemAdded(Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSubItemAdded(item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnSubItemAdded(item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnSubItemRemoved(Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSubItemRemoved(item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnSubItemRemoved(item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnItemBounceCleared(Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSubItemBounceCleared(item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnSubItemBounceCleared(item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnSubItemBounceCleared(Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSubItemBounceCleared(item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnSubItemBounceCleared(item);
|
|
}
|
|
}
|
|
|
|
public virtual bool CheckTarget(Mobile from, Target targ, object targeted) =>
|
|
m_Parent switch
|
|
{
|
|
Item item => item.CheckTarget(from, targ, targeted),
|
|
Mobile mobile => mobile.CheckTarget(from, targ, targeted),
|
|
_ => true
|
|
};
|
|
|
|
public virtual bool IsAccessibleTo(Mobile check)
|
|
{
|
|
if (m_Parent is Item item)
|
|
{
|
|
return item.IsAccessibleTo(check);
|
|
}
|
|
|
|
var reg = Region.Find(GetWorldLocation(), m_Map);
|
|
|
|
return reg.CheckAccessibility(this, check);
|
|
|
|
/*SecureTradeContainer cont = GetSecureTradeCont();
|
|
|
|
if (cont != null && !cont.IsChildOf( check ))
|
|
return false;
|
|
|
|
return true;*/
|
|
}
|
|
|
|
public bool IsChildOf(IEntity o) => IsChildOf(o, false);
|
|
|
|
public bool IsChildOf(IEntity o, bool allowNull)
|
|
{
|
|
var p = m_Parent;
|
|
|
|
if ((p == null || o == null) && !allowNull)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (p == o)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
while (p is Item item)
|
|
{
|
|
if (item.m_Parent == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
p = item.m_Parent;
|
|
|
|
if (p == o)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual void OnItemUsed(Mobile from, Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnItemUsed(from, item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnItemUsed(from, item);
|
|
}
|
|
}
|
|
|
|
public bool CheckItemUse(Mobile from) => CheckItemUse(from, this);
|
|
|
|
public virtual bool CheckItemUse(Mobile from, Item item) =>
|
|
m_Parent switch
|
|
{
|
|
Item parentItem => parentItem.CheckItemUse(from, item),
|
|
Mobile parentMobile => parentMobile.CheckItemUse(from, item),
|
|
_ => true
|
|
};
|
|
|
|
public virtual void OnItemLifted(Mobile from, Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnItemLifted(from, item);
|
|
}
|
|
else if (m_Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.OnItemLifted(from, item);
|
|
}
|
|
}
|
|
|
|
public bool CheckLift(Mobile from)
|
|
{
|
|
var reject = LRReason.Inspecific;
|
|
|
|
return CheckLift(from, this, ref reject);
|
|
}
|
|
|
|
public virtual bool CheckLift(Mobile from, Item item, ref LRReason reject) =>
|
|
m_Parent switch
|
|
{
|
|
Item parentItem => parentItem.CheckLift(from, item, ref reject),
|
|
Mobile parentMobile => parentMobile.CheckLift(from, item, ref reject),
|
|
_ => true
|
|
};
|
|
|
|
public virtual void OnSingleClickContained(Mobile from, Item item)
|
|
{
|
|
if (m_Parent is Item parentItem)
|
|
{
|
|
parentItem.OnSingleClickContained(from, item);
|
|
}
|
|
}
|
|
|
|
public virtual void OnAosSingleClick(Mobile from)
|
|
{
|
|
var opl = PropertyList;
|
|
|
|
if (opl.Header > 0)
|
|
{
|
|
from.NetState.SendMessageLocalized(
|
|
Serial,
|
|
m_ItemID,
|
|
MessageType.Label,
|
|
0x3B2,
|
|
3,
|
|
opl.Header,
|
|
Name,
|
|
opl.HeaderArgs
|
|
);
|
|
}
|
|
}
|
|
|
|
public virtual void OnSingleClick(Mobile from)
|
|
{
|
|
if (Deleted || !from.CanSee(this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (DisplayLootType)
|
|
{
|
|
LabelLootTypeTo(from);
|
|
}
|
|
|
|
var ns = from.NetState;
|
|
|
|
if (ns.CannotSendPackets())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Name == null)
|
|
{
|
|
if (m_Amount <= 1)
|
|
{
|
|
ns.SendMessageLocalized(Serial, m_ItemID, MessageType.Label, 0x3B2, 3, LabelNumber);
|
|
}
|
|
else
|
|
{
|
|
ns.SendMessageLocalizedAffix(
|
|
Serial,
|
|
m_ItemID,
|
|
MessageType.Label,
|
|
0x3B2,
|
|
3,
|
|
LabelNumber,
|
|
"",
|
|
AffixType.Append,
|
|
$" : {m_Amount}"
|
|
);
|
|
}
|
|
}
|
|
else if (m_Amount > 1)
|
|
{
|
|
ns.SendMessage(
|
|
Serial,
|
|
m_ItemID,
|
|
MessageType.Label,
|
|
0x3B2,
|
|
3,
|
|
false,
|
|
"ENU",
|
|
"",
|
|
$"{Name} : {m_Amount}"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
ns.SendMessage(
|
|
Serial,
|
|
m_ItemID,
|
|
MessageType.Label,
|
|
0x3B2,
|
|
3,
|
|
false,
|
|
"ENU",
|
|
"",
|
|
$"{Name}"
|
|
);
|
|
}
|
|
}
|
|
|
|
public virtual void ScissorHelper(Mobile from, Item newItem, int amountPerOldItem, bool carryHue = true)
|
|
{
|
|
// let's not go over 60000
|
|
var amount = Math.Min(Amount, 60000 / amountPerOldItem);
|
|
|
|
var ourHue = Hue;
|
|
var thisMap = Map;
|
|
var thisParent = m_Parent;
|
|
var worldLoc = GetWorldLocation();
|
|
var type = LootType;
|
|
|
|
Consume(amount);
|
|
|
|
newItem.Amount = amount * amountPerOldItem;
|
|
|
|
if (carryHue)
|
|
{
|
|
newItem.Hue = ourHue;
|
|
}
|
|
|
|
if (ScissorCopyLootType)
|
|
{
|
|
newItem.LootType = type;
|
|
}
|
|
|
|
if ((thisParent as Container)?.TryDropItem(from, newItem, false) != true)
|
|
{
|
|
newItem.MoveToWorld(worldLoc, thisMap);
|
|
}
|
|
}
|
|
|
|
public virtual void Consume(int amount = 1)
|
|
{
|
|
if (Amount <= amount)
|
|
{
|
|
Delete();
|
|
return;
|
|
}
|
|
|
|
Amount -= amount;
|
|
}
|
|
|
|
public virtual void ReplaceWith(Item newItem)
|
|
{
|
|
if (m_Parent is Container container)
|
|
{
|
|
container.AddItem(newItem);
|
|
newItem.Location = m_Location;
|
|
}
|
|
else
|
|
{
|
|
newItem.MoveToWorld(GetWorldLocation(), m_Map);
|
|
}
|
|
|
|
Delete();
|
|
}
|
|
|
|
public virtual bool CheckBlessed(Mobile m) =>
|
|
m_LootType == LootType.Blessed || ServerFeatureFlags.InsuranceEnabled && Insured || m != null && m == BlessedFor;
|
|
|
|
public virtual bool CheckNewbied() => m_LootType == LootType.Newbied;
|
|
|
|
public virtual bool IsStandardLoot() =>
|
|
(!ServerFeatureFlags.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 DateTime m_DecayReset;
|
|
|
|
public int m_TempFlags;
|
|
|
|
public double m_Weight = -1;
|
|
|
|
public int Version;
|
|
}
|
|
|
|
[Flags]
|
|
private enum SaveFlag : uint
|
|
{
|
|
None = 0x00000000,
|
|
Direction = 0x00000001,
|
|
Bounce = 0x00000002,
|
|
LootType = 0x00000004,
|
|
LocationFull = 0x00000008,
|
|
ItemID = 0x00000010,
|
|
Hue = 0x00000020,
|
|
Amount = 0x00000040,
|
|
Layer = 0x00000080,
|
|
Name = 0x00000100,
|
|
Parent = 0x00000200,
|
|
Items = 0x00000400,
|
|
WeightNot1or0 = 0x00000800,
|
|
Map = 0x00001000,
|
|
Visible = 0x00002000,
|
|
Movable = 0x00004000,
|
|
Stackable = 0x00008000,
|
|
WeightIs0 = 0x00010000,
|
|
LocationSByteZ = 0x00020000,
|
|
LocationShortXY = 0x00040000,
|
|
LocationByteXY = 0x00080000,
|
|
ImplFlags = 0x00100000,
|
|
InsuredFor = 0x00200000,
|
|
BlessedFor = 0x00400000,
|
|
HeldBy = 0x00800000,
|
|
IntWeight = 0x01000000,
|
|
SavedFlags = 0x02000000,
|
|
NullWeight = 0x04000000,
|
|
PlayerConstructed = 0x08000000,
|
|
DecayReset = 0x10000000
|
|
}
|
|
}
|