ModernUO/Projects/Server/Items/Item.cs
Kamron Batman e12cc5dd83
perf(saves): eliminate world-save freeze bottlenecks (~9.5x faster freeze) (#2525)
Reduces the world-save freeze window from ~740ms to ~78ms (measured on a synthetic 10M-entity / 1.7GB world, 24 cores, through the real pipeline classes) by removing the per-entity handoff between the game loop and the serialization workers, fixing how large indivisible payloads are scheduled, rewriting the BufferWriter hot path, removing per-entity placement state entirely, and finally replacing the global serialized-types tracking with a per-file type table (idx v4) that also shrinks idx files by ~21% and speeds the background write phase. The pipeline has also been validated end-to-end on live-copy worlds in the multi-million-entity range, where the freeze is drain-bound (real `Serialize()` costs far more CPU per byte than synthetic writes) — the same structural wins hold, and entity/file round-trips are byte-clean across both load paths.

## The problem

The freeze window is `max(main-thread handoff, slowest worker drain)`:

1. **The producer was the bottleneck.** The main thread round-robined every entity through per-worker `ConcurrentQueue`s — two interlocked ops per entity, ~740ms of freeze floor at 10M entities before any serialization happened.
2. **Round-robin distributes count, not cost.** "Deep" systems (50MB generic persistence blobs) and "thick" entities (100K-item storage keys) landed on arbitrary workers, producing lopsided drain times on large worlds.
3. **Worst-case scheduling.** `GenericEntityPersistence.Serialize` pushed its self-payload *after* all entities, and generic persistences sort last in the registry — so the biggest indivisible blobs started serializing at the very end, extending the freeze by their entire duration.

## The fix

**Commit 1 — chunked handoff + LPT scheduling + heap pre-sizing:**
- Pooled 4096-entity chunks published to one shared queue; workers pull chunks and load-balance dynamically (a worker busy with a thick entity simply takes fewer chunks).
- `Persistence.SerializeAll` pushes systems largest-first (LPT) using the previous save's payload size (or loaded file size on first boot); self-payloads get dedicated single-entity chunks so they overlap the entity stream instead of ending it.
- Worker heaps pre-size from the loaded save's `.bin` totals, eliminating copy-on-grow inside the first save's freeze.
- `SpinWait` backoff in the drain loop (never `Sleep(1)`), per-worker balance stats logged in debug builds (the call site is compiled out of Release), 1MB snapshot write buffer.

**Commit 2 — workers iterate the dictionaries directly + main thread joins the drain:**
- `GenericEntityPersistence` publishes 4096-slot ranges over its dictionary's backing entries array; workers serialize occupied slots (`value != null`) directly through a `ShadowEntry<TValue>` struct mirroring the runtime's private `Entry` layout. Safe because the dictionary is frozen during `Saving` (mutations divert to the pending safety queues).
- The layout is **proven at startup before any code reads through it**: validation measures the true `Entry` stride via precise allocation accounting (guaranteeing all shadow reads are in-bounds), then verifies every key/value of a churned, resized, freelist-exercised dictionary — reading value slots as raw pointer bits only, never materializing a managed reference until the layout is proven. If a future runtime changes `Dictionary` internals, validation fails with a logged warning and saves fall back to the (fully maintained) enumerate-and-push path.
- The main thread joins the drain via an inline worker after publishing, instead of idling — worth a full worker share, proportionally more on low-core hosts.

**Commit 3 — 2.2x faster BufferWriter write path, single-pass short strings:**
- PGO already devirtualizes and inlines every `IGenericWriter.Write` callsite (interface vs concrete measured identical) — the real per-write cost was the non-inlinable `Index` setter (range-check throw path + per-write high-water tracking) plus span bounds checks. Writes now reserve capacity once, then do an unaligned store through a ref with a raw index increment; the high-water mark folds at Seek/Resize instead of per write.
- Class-level implementations of the hottest default interface methods keep nested writes inlined (a DIM re-dispatches on `this` even at a devirtualized callsite).
- Strings of 85 chars or fewer encode once into a stack scratch instead of walking the string twice (`GetByteCount` + `GetBytes`). Byte output is identical.
- Measured: 34.4 → 15.7 ns/entity on a generated-style write mix; end-to-end freeze ~99ms → ~74-82ms.

**Commit 4 — branch-free fallback push loop:**
- A bare `foreach { PushToCache(entity); }` runs at 2.3ns/entity; the same loop carrying a per-entity heavy-check runs 2.3x slower — the cost is the fatter loop body defeating tight-loop codegen. Entity-level >1MB payloads are rare enough to ride in shared chunks; system self-payloads (the large ones) are still explicitly scheduled largest-first.

**Commit 5 — drop the 9-byte per-entity placement state; snapshots write from worker segment logs:**
- Every `ISerializable` carried `SerializedThread/SerializedPosition/SerializedLength` so `WriteSnapshot` could gather each entity's bytes from the worker heaps in dictionary order. But the idx records absolute positions — bin order is free — so the snapshot is now written in worker-heap order and the join inverts: workers log segments (owner, slot range, heap start) plus one length per record as they serialize; positions are implicit because a worker's writes are contiguous, and identity comes from re-walking the same snapshot slots in the same order (stable until `PostWorldSave`).
- Chunks are persistence-homogeneous (the partial chunk publishes at each `SerializeAll` boundary) so segments route to files by owner with zero per-entity state. Self-payloads keep placement as three private fields on the handful of persistence instances.
- Net: 9 bytes (plus padding) of resident state removed from every item, mobile, guild, and account on every shard; three interface-property stores per entity leave the drain hot path (stamping dirtied one cache line per entity mid-freeze — the lengths log is one sequential stream); each segment's bytes hit the bin as a single span write instead of one copy per entity, speeding the background write phase; and `IGenericSerializable` shrinks to just `Serialize(IGenericWriter)`. Transient cost: ~4 bytes per entity in pooled per-worker logs, released after each write. The save format was unchanged at this point (idx v3, same loader); the v4 bump comes later in the branch.

**Commit 6 — staged file writes replace memory-mapped snapshot writing:**
- `MemoryMapFileWriter` is removed. `FileBufferWriter` composes through the full `BufferWriter` raw write path into a pooled staging block that drains to the file as large sequential positional writes (`RandomAccess.Write`); seeks flush the block and move the file offset, so backwards patches (the idx entity count) become small positional writes.
- Memory-mapped composition paid a soft page fault on every composed page plus unpredictable dirty-section teardown stalls at dispose — measured ~4x slower end-to-end than staged writes at snapshot sizes.

**Commits 7–11 — idx v4: per-file type table replaces SerializedTypes.db and all runtime type tracking:**
- Previously every `Write(Type)` from every worker enqueued into a shared `ConcurrentQueue<Type>` during the freeze (interlocked writes on a shared cache line, millions of mostly-duplicate entries), the background phase drained and deduped it all into a `HashSet`, and the snapshot recomputed `xxHash64(Type.FullName)` once per entity record (~5.4M redundant hashes per save on a large world) to write 9-byte tag+hash idx records plus a global `SerializedTypes.db`.
- The db's only real job was diagnostics: the string name behind "Type `<X>` was not found. Delete all of those types?" during idx loading. That map now lives in the idx itself: each `GenericEntityPersistence<T>` keeps an insertion-ordered `Type -> ushort` table, hydrated at `AddEntity` and on every deserialize path — one dictionary `TryAdd` per entity add on the game thread, amortized across gameplay, and provably immutable while the background writer reads it (adds divert to the pending queues during saves).
- idx v4 layout: the table (names only) is written before the records; records reference it by 2-byte index, shrinking from 33 to 26 bytes (−21%). The loader resolves each table name **once** (`FindTypeByHash(ComputeHash64(name))` — semantically identical to v3 resolution, `TypeAlias` included) into a constructor array, and each record becomes an array index instead of an 8-byte hash read plus dictionary probe. The unresolved-type prompt now surfaces once per type, with the name.
- Deleted outright: `World.SerializedTypes`, the drain/dedupe pass in `WriteFiles`, `BufferWriter`'s type tracking (its `Write(Type)` is now pure — payload format unchanged: tag byte + xxHash64), `FileBufferWriter`'s typeSet parameter, `Persistence.WriteSerializedTypesSnapshot`, and the adhoc db write. SerializedTypes.db is no longer produced.
- **Backward compatibility:** v0–v3 saves (including their SerializedTypes.db and legacy tdb files) load exactly as before, and every legacy load path hydrates the new table so the first v4 save after an upgrade is complete. Stale db files in existing save folders are simply ignored. Verified live: a v3 save boots, saves as v4 (Items.idx −20.2% on a dev world), and reloads with identical entity counts.

## Measured (synthetic 10M entities / 1.7GB, 64+64+32MB system blobs, 24x 2MB thick entities, dense write profile, 24 cores)

| Metric | Before | After |
|---|---|---|
| Steady-state freeze | ~740 ms | **~78 ms** |
| Main-thread publish cost | ~740 ms | **~0.1 ms** |
| Steady-state allocations | 0 | 0 (by iter 2) |
| Worker byte-load spread | 2x | ~1.15x |

The freeze is now bound by pure serialize throughput (payload / cores).

## Tests

- 779 Server.Tests + 501 UOContent.Tests pass.
- New across the branch: chunk fill/flush/owner-boundary tests, pool reuse/clear tests, an end-to-end multi-worker drain through the real wake/push/flush/pause protocol, a 50K-entry churn equivalence test for the shadow iteration re-walk (the exact pairing the snapshot writer relies on), byte-level BufferWriter output/position pins, `RuntimeLayoutIsSupported` so a silent fallback on a future runtime upgrade fails loudly in CI, and a full snapshot **round-trip test** that serializes 25K entities plus a self-payload through real workers, writes the idx/bin from the segment logs, and reloads them through the standard loader (now in v4 format).
- For idx v4 specifically: `FileBufferWriter` staging/drain/seek-patch and oversized-item tests, type-table registration tests, a hand-written v4 fixture proving an unresolvable type name skips only its own records through the console confirmation flow, and a hand-written **legacy v3 fixture** proving old saves still load and hydrate the type table for their next save.

## Trade-offs

- Chunk scheduling is nondeterministic, so worker heaps ratchet to each worker's max-ever draw rather than a fixed share. With slot ranges the balance is tight (~1.15x), so the effect is small; a shared slab pool remains an option if production shows retention creep.
- Entity-level heavy items inside slot ranges are serialized wherever they're encountered (no LPT for them); worst-case tail is one thick entity's serialize time (~ms). System self-payloads — the large ones — are still explicitly scheduled largest-first.
- Snapshot-write error granularity is per segment rather than per entity (heap-bounds bugs were the only thing the per-entity catch ever caught; idx metadata reads keep per-record granularity).
- idx v4 is a save-format version bump: old saves load unchanged through the preserved legacy paths, but saves written by this branch require this loader. Per-persistence type tables cap at 65,535 distinct entity types per boot (hard throw, orders of magnitude of headroom), and a type's table slot persists until restart even if its last entity is deleted — a few stale name entries per file, by design.
2026-07-16 22:53:28 -07:00

4333 lines
114 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.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;
public DateTime LastMoved { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public bool Stackable
{
get => GetFlag(ImplFlag.Stackable);
set => SetFlag(ImplFlag.Stackable, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public bool Visible
{
get => GetFlag(ImplFlag.Visible);
set
{
if (GetFlag(ImplFlag.Visible) != value)
{
SetFlag(ImplFlag.Visible, value);
if (m_Map != null)
{
var worldLoc = GetWorldLocation();
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
{
var m = state.Mobile;
if (!m.CanSee(this) && m.InRange(worldLoc, GetUpdateRange(m)))
{
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
state.Send(removeEntity);
}
}
}
Delta(ItemDelta.Update);
UpdateDecayRegistration();
}
}
}
[CommandProperty(AccessLevel.GameMaster)]
public bool Movable
{
get => GetFlag(ImplFlag.Movable);
set
{
if (GetFlag(ImplFlag.Movable) != value)
{
SetFlag(ImplFlag.Movable, value);
Delta(ItemDelta.Update);
UpdateDecayRegistration();
}
}
}
public virtual bool ForceShowProperties => false;
public virtual bool HandlesOnMovement => false;
public static int LockedDownFlag { get; set; }
public static int SecureFlag { get; set; }
[IgnoreDupe]
public bool IsLockedDown
{
get => GetTempFlag(LockedDownFlag);
set
{
SetTempFlag(LockedDownFlag, value);
InvalidateProperties();
}
}
[IgnoreDupe]
public bool IsSecure
{
get => GetTempFlag(SecureFlag);
set
{
SetTempFlag(SecureFlag, value);
InvalidateProperties();
}
}
public virtual bool IsVirtualItem => false;
public virtual bool CanSeeStaffOnly(Mobile from) => from.AccessLevel > AccessLevel.Counselor;
public virtual int LabelNumber
{
get
{
if (m_ItemID < 0x4000)
{
return 1020000 + m_ItemID;
}
return 1078872 + m_ItemID;
}
}
[CommandProperty(AccessLevel.GameMaster)]
public int TotalGold => GetTotal(TotalType.Gold);
[CommandProperty(AccessLevel.GameMaster)]
public int TotalItems => GetTotal(TotalType.Items);
[CommandProperty(AccessLevel.GameMaster)]
public int TotalWeight => GetTotal(TotalType.Weight);
public virtual double DefaultWeight
{
get
{
if (m_ItemID < 0 || m_ItemID > TileData.MaxItemValue || this is BaseMulti)
{
return 0;
}
var weight = TileData.ItemTable[m_ItemID].Weight;
if (weight is 255 or 0)
{
weight = 1;
}
return weight;
}
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public double Weight
{
get
{
var info = LookupCompactInfo();
return info is { m_Weight: >= 0 } ? info.m_Weight : DefaultWeight;
}
set
{
var defaultWeight = DefaultWeight;
var info = LookupCompactInfo();
var oldWeight = info is { m_Weight: >= 0 } ? info.m_Weight : defaultWeight;
if (oldWeight == value)
{
return;
}
if (value < 0 || defaultWeight == value)
{
info?.m_Weight = -1;
VerifyCompactInfo();
}
else
{
(info ?? AcquireCompactInfo()).m_Weight = value;
}
UpdateTotal(this, TotalType.Weight, PileWeight - (int)Math.Ceiling(oldWeight * Amount));
InvalidateProperties();
}
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public int PileWeight => (int)Math.Ceiling(Weight * Amount);
[Hue]
[CommandProperty(AccessLevel.GameMaster)]
public virtual int Hue
{
get => m_Hue;
set
{
if (m_Hue != value)
{
m_Hue = value;
Delta(ItemDelta.Update);
}
}
}
public virtual bool Nontransferable => QuestItem;
[CommandProperty(AccessLevel.GameMaster)]
public virtual Layer Layer
{
get => m_Layer;
set
{
if (m_Layer != value)
{
m_Layer = value;
Delta(ItemDelta.EquipOnly);
}
}
}
// SendMobileIncoming (0x78) sends only the first item per layer (it dedupes via the
// `layers` span). Equipment Layer comes from tiledata, so e.g. a two-handed weapon and a
// shield both resolve to Layer.TwoHanded; a later same-layer item sent on its own via
// EquipUpdate (0x2E) or its OPL would leave the client holding two items on one equipment
// slot (a use-after-free on the legacy 2D client when that slot is later torn down).
// Returns true if this equipped item is such a later duplicate and must not be sent alone.
public bool IsDupedEquipLayer() => m_Parent is Mobile m && m.FindItemOnLayer(m_Layer) != this;
public List<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; }
[CommandProperty(AccessLevel.GameMaster)]
public bool QuestItem
{
get => GetFlag(ImplFlag.QuestItem);
set
{
SetFlag(ImplFlag.QuestItem, value);
InvalidateProperties();
Delta(ItemDelta.Update);
}
}
public bool Insured
{
get => GetFlag(ImplFlag.Insured);
set
{
SetFlag(ImplFlag.Insured, value);
InvalidateProperties();
}
}
public bool PaidInsurance
{
get => GetFlag(ImplFlag.PaidInsurance);
set => SetFlag(ImplFlag.PaidInsurance, value);
}
public Mobile BlessedFor
{
get => LookupCompactInfo()?.m_BlessedFor;
set
{
var info = AcquireCompactInfo();
info.m_BlessedFor = value;
if (info.m_BlessedFor == null)
{
VerifyCompactInfo();
}
InvalidateProperties();
}
}
public int CompareTo(Item other) => other == null ? -1 : Serial.CompareTo(other.Serial);
public virtual int HuedItemID => m_ItemID;
public ObjectPropertyList PropertyList => m_PropertyList ??= InitializePropertyList(new ObjectPropertyList(this));
/// <summary>
/// Overridable. Fills an <see cref="ObjectPropertyList" /> with everything applicable. By default, this invokes
/// <see cref="AddNameProperties" />, then <see cref="Item.GetChildProperties">Item.GetChildProperties</see> or
/// <see cref="Mobile.GetChildProperties">Mobile.GetChildProperties</see>. This method should be overridden to add any
/// custom
/// properties.
/// </summary>
public virtual void GetProperties(IPropertyList list)
{
AddNameProperties(list);
AppendChildNameProperties(list);
}
[IgnoreDupe]
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now;
[IgnoreDupe]
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
public virtual void Serialize(IGenericWriter writer)
{
writer.Write(9); // version
var flags = SaveFlag.None;
int x = m_Location.m_X, y = m_Location.m_Y, z = m_Location.m_Z;
if (x != 0 || y != 0 || z != 0)
{
if (x is >= short.MinValue and <= short.MaxValue && y is >= short.MinValue and <= short.MaxValue &&
z is >= sbyte.MinValue and <= sbyte.MaxValue)
{
if (x != 0 || y != 0)
{
if (x is >= byte.MinValue and <= byte.MaxValue && y is >= byte.MinValue and <= byte.MaxValue)
{
flags |= SaveFlag.LocationByteXY;
}
else
{
flags |= SaveFlag.LocationShortXY;
}
}
if (z != 0)
{
flags |= SaveFlag.LocationSByteZ;
}
}
else
{
flags |= SaveFlag.LocationFull;
}
}
var info = LookupCompactInfo();
var items = LookupItems();
if (m_Direction != Direction.North)
{
flags |= SaveFlag.Direction;
}
if (info?.m_Bounce != null)
{
flags |= SaveFlag.Bounce;
}
if (m_LootType != LootType.Regular)
{
flags |= SaveFlag.LootType;
}
if (m_ItemID != 0)
{
flags |= SaveFlag.ItemID;
}
if (m_Hue != 0)
{
flags |= SaveFlag.Hue;
}
if (m_Amount != 1)
{
flags |= SaveFlag.Amount;
}
if (m_Layer != Layer.Invalid)
{
flags |= SaveFlag.Layer;
}
if (info?.m_Name != null)
{
flags |= SaveFlag.Name;
}
if (m_Parent != null)
{
flags |= SaveFlag.Parent;
}
if (items.Count > 0)
{
flags |= SaveFlag.Items;
}
if (m_Map != Map.Internal)
{
flags |= SaveFlag.Map;
}
// if (m_InsuredFor != null && !m_InsuredFor.Deleted)
// flags |= SaveFlag.InsuredFor;
if (info != null)
{
if (info.m_BlessedFor?.Deleted == false)
{
flags |= SaveFlag.BlessedFor;
}
if (info.m_HeldBy?.Deleted == false)
{
flags |= SaveFlag.HeldBy;
}
if (info.m_SavedFlags != 0)
{
flags |= SaveFlag.SavedFlags;
}
}
if (info == null || info.m_Weight < 0)
{
flags |= SaveFlag.NullWeight;
}
else if (info.m_Weight == 0.0)
{
flags |= SaveFlag.WeightIs0;
}
else if (info.m_Weight > 0)
{
if (info.m_Weight == (int)info.m_Weight)
{
flags |= SaveFlag.IntWeight;
}
else
{
flags |= SaveFlag.WeightNot1or0;
}
}
var implFlags = m_Flags & (ImplFlag.Visible | ImplFlag.Movable | ImplFlag.Stackable | ImplFlag.Insured |
ImplFlag.PaidInsurance | ImplFlag.QuestItem);
if (implFlags != (ImplFlag.Visible | ImplFlag.Movable))
{
flags |= SaveFlag.ImplFlags;
}
writer.Write((int)flags);
/* begin last moved time optimization */
var ticks = LastMoved.Ticks;
var now = Core.Now.Ticks;
var minutes = new TimeSpan(now - ticks).TotalMinutes;
writer.WriteEncodedInt((int)Math.Clamp(minutes, int.MinValue, int.MaxValue));
/* end */
if (GetSaveFlag(flags, SaveFlag.Direction))
{
writer.Write((byte)m_Direction);
}
if (GetSaveFlag(flags, SaveFlag.Bounce))
{
BounceInfo.Serialize(info?.m_Bounce, writer);
}
if (GetSaveFlag(flags, SaveFlag.LootType))
{
writer.Write((byte)m_LootType);
}
if (GetSaveFlag(flags, SaveFlag.LocationFull))
{
writer.WriteEncodedInt(x);
writer.WriteEncodedInt(y);
writer.WriteEncodedInt(z);
}
else
{
if (GetSaveFlag(flags, SaveFlag.LocationByteXY))
{
writer.Write((byte)x);
writer.Write((byte)y);
}
else if (GetSaveFlag(flags, SaveFlag.LocationShortXY))
{
writer.Write((short)x);
writer.Write((short)y);
}
if (GetSaveFlag(flags, SaveFlag.LocationSByteZ))
{
writer.Write((sbyte)z);
}
}
if (GetSaveFlag(flags, SaveFlag.ItemID))
{
writer.WriteEncodedInt(m_ItemID);
}
if (GetSaveFlag(flags, SaveFlag.Hue))
{
writer.WriteEncodedInt(m_Hue);
}
if (GetSaveFlag(flags, SaveFlag.Amount))
{
writer.WriteEncodedInt(m_Amount);
}
if (GetSaveFlag(flags, SaveFlag.Layer))
{
writer.Write((byte)m_Layer);
}
if (GetSaveFlag(flags, SaveFlag.Name))
{
writer.Write(info.m_Name);
}
if (GetSaveFlag(flags, SaveFlag.Parent))
{
writer.Write(m_Parent);
}
if (GetSaveFlag(flags, SaveFlag.Items))
{
writer.Write(items);
}
if (GetSaveFlag(flags, SaveFlag.IntWeight))
{
writer.WriteEncodedInt((int)info.m_Weight);
}
else if (GetSaveFlag(flags, SaveFlag.WeightNot1or0))
{
writer.Write(info.m_Weight);
}
if (GetSaveFlag(flags, SaveFlag.Map))
{
writer.Write(m_Map);
}
if (GetSaveFlag(flags, SaveFlag.ImplFlags))
{
writer.WriteEncodedInt((int)implFlags);
}
if (GetSaveFlag(flags, SaveFlag.InsuredFor))
{
writer.Write((Mobile)null);
}
if (GetSaveFlag(flags, SaveFlag.BlessedFor))
{
writer.Write(info.m_BlessedFor);
}
if (GetSaveFlag(flags, SaveFlag.HeldBy))
{
writer.Write(info.m_HeldBy);
}
if (GetSaveFlag(flags, SaveFlag.SavedFlags))
{
writer.WriteEncodedInt(info.m_SavedFlags);
}
}
public void MoveToWorld(WorldLocation worldLocation)
{
MoveToWorld(worldLocation.Location, worldLocation.Map);
}
/// <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 (old == null || old == Map.Internal)
{
InvalidateProperties();
}
}
}
}
public virtual void ProcessDelta()
{
var flags = m_DeltaFlags;
SetFlag(ImplFlag.InQueue, false);
m_DeltaFlags = ItemDelta.None;
var map = m_Map;
if (map == null || Deleted)
{
return;
}
var worldLoc = GetWorldLocation();
var update = (flags & ItemDelta.Update) != 0;
if (update && m_Parent is Container { IsPublicContainer: false } contParent)
{
var rootParent = contParent.RootParent as Mobile;
Mobile tradeRecip = null;
if (rootParent != null)
{
var ns = rootParent.NetState;
if (ns != null && rootParent.CanSee(this) && rootParent.InRange(worldLoc, GetUpdateRange(rootParent)))
{
ns.SendContainerContentUpdate(this);
SendOPLPacketTo(ns);
}
}
var st = GetSecureTradeCont()?.Trade;
if (st != null)
{
var test = st.From.Mobile;
if (test != null && test != rootParent)
{
tradeRecip = test;
}
test = st.To.Mobile;
if (test != null && test != rootParent)
{
tradeRecip = test;
}
var ns = tradeRecip?.NetState;
if (ns != null && tradeRecip.CanSee(this) && tradeRecip.InRange(worldLoc, GetUpdateRange(tradeRecip)))
{
ns.SendContainerContentUpdate(this);
SendOPLPacketTo(ns);
}
}
var openers = contParent.Openers;
if (openers != null)
{
for (var i = 0; i < openers.Count; ++i)
{
var mob = openers[i];
var range = GetUpdateRange(mob);
if (mob.Map != map || !mob.InRange(worldLoc, range))
{
openers.RemoveAt(i--);
}
else
{
if (mob == rootParent || mob == tradeRecip)
{
continue;
}
var ns = mob.NetState;
if (ns != null && mob.CanSee(this))
{
ns.SendContainerContentUpdate(this);
SendOPLPacketTo(ns);
}
}
}
if (openers.Count == 0)
{
contParent.Openers = null;
}
}
return;
}
// A second item sharing an equipment layer is omitted by SendMobileIncoming (0x78);
// sending it on its own via EquipUpdate/OPL would leave the client with two items on
// one slot. Skip the per-client sends entirely for the dupe.
if (IsDupedEquipLayer())
{
return;
}
foreach (var state in map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
{
var m = state.Mobile;
if (!m.CanSee(this) || !m.InRange(worldLoc, GetUpdateRange(m)))
{
continue;
}
if (update)
{
if (m_Parent == null)
{
SendInfoTo(state);
}
else
{
if (m_Parent is Item)
{
// TODO: Optimize by writing once?
state.SendContainerContentUpdate(this);
}
else if (m_Parent is Mobile)
{
// TODO: Optimize by writing once?
state.SendEquipUpdate(this);
}
SendOPLPacketTo(state);
}
}
else if ((flags & ItemDelta.EquipOnly) != 0 && m_Parent is Mobile)
{
state.SendEquipUpdate(this);
SendOPLPacketTo(state);
}
else if (ObjectPropertyList.Enabled && (flags & ItemDelta.Properties) != 0)
{
SendOPLPacketTo(state);
}
}
}
public virtual void Delete()
{
if (Deleted)
{
return;
}
OnDelete();
DecayScheduler.Unregister(this);
var items = LookupItems();
for (var i = items.Count - 1; i >= 0; --i)
{
if (i < items.Count)
{
items[i].OnParentDeleted(this);
}
}
SendRemovePacket();
SetFlag(ImplFlag.Deleted, true);
if (Parent is Mobile mobile)
{
mobile.RemoveItem(this);
}
else if (Parent is Item item)
{
item.RemoveItem(this);
}
ClearBounce();
if (m_Map != null)
{
if (m_Parent == null)
{
m_Map.OnLeave(this);
}
m_Map = null;
}
World.RemoveEntity(this);
OnAfterDelete();
ClearProperties();
}
public virtual bool SkipSerialization => false;
[IgnoreDupe]
public ISpawner Spawner
{
get => LookupCompactInfo()?.m_Spawner;
set
{
var info = AcquireCompactInfo();
var oldValue = info.m_Spawner;
info.m_Spawner = value;
if (info.m_Spawner == null)
{
VerifyCompactInfo();
}
if (oldValue != value)
{
UpdateDecayRegistration();
}
}
}
public virtual void OnBeforeSpawn(Point3D location, Map m)
{
}
public virtual void OnAfterSpawn()
{
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public virtual Point3D Location
{
get => m_Location;
set
{
var oldLocation = m_Location;
if (oldLocation == value)
{
return;
}
if (m_Map != null)
{
if (m_Parent == null)
{
if (m_Location.m_X != 0)
{
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
foreach (var state in m_Map.GetClientsInRange(oldLocation, GetMaxUpdateRange()))
{
var m = state.Mobile;
if (!m.InRange(value, GetUpdateRange(m)))
{
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
state.Send(removeEntity);
}
}
}
var oldLoc = m_Location;
m_Location = value;
SetLastMoved();
foreach (var state in m_Map.GetClientsInRange(m_Location, GetMaxUpdateRange()))
{
var m = state.Mobile;
if (m.CanSee(this) && m.InRange(m_Location, GetUpdateRange(m)) &&
(!state.HighSeas || !NoMoveHS || (m_DeltaFlags & ItemDelta.Update) != 0 ||
!m.InRange(oldLoc, GetUpdateRange(m))))
{
SendInfoTo(state);
}
}
RemDelta(ItemDelta.Update);
}
else
{
m_Location = value;
if (m_Parent is Item)
{
Delta(ItemDelta.Update);
}
}
if (m_Parent == null)
{
m_Map.OnMove(oldLocation, this);
}
}
else
{
m_Location = value;
}
OnLocationChange(oldLocation);
}
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public int X
{
get => Location.m_X;
set => Location = new Point3D(value, m_Location.m_Y, m_Location.m_Z);
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public int Y
{
get => Location.m_Y;
set => Location = new Point3D(m_Location.m_X, value, m_Location.m_Z);
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public int Z
{
get => Location.m_Z;
set => Location = new Point3D(m_Location.m_X, m_Location.m_Y, value);
}
public virtual bool InRange(Point2D p, int range) => Utility.InRange(p.X, p.Y, X, Y, range);
public virtual bool InRange(Point3D p, int range) => Utility.InRange(p.X, p.Y, X, Y, range);
public ExpandFlag GetExpandFlags()
{
var info = LookupCompactInfo();
ExpandFlag flags = 0;
if (info != null)
{
if (info.m_BlessedFor != null)
{
flags |= ExpandFlag.Blessed;
}
if (info.m_Bounce != null)
{
flags |= ExpandFlag.Bounce;
}
if (info.m_HeldBy != null)
{
flags |= ExpandFlag.Holder;
}
if (info.m_Items != null)
{
flags |= ExpandFlag.Items;
}
if (info.m_Name != null)
{
flags |= ExpandFlag.Name;
}
if (info.m_Spawner != null)
{
flags |= ExpandFlag.Spawner;
}
if (info.m_SavedFlags != 0)
{
flags |= ExpandFlag.SaveFlag;
}
if (info.m_TempFlags != 0)
{
flags |= ExpandFlag.TempFlag;
}
if (info.m_Weight >= 0)
{
flags |= ExpandFlag.Weight;
}
}
return flags;
}
private CompactInfo LookupCompactInfo() => m_CompactInfo;
private CompactInfo AcquireCompactInfo() => m_CompactInfo ??= new CompactInfo();
private void ReleaseCompactInfo()
{
m_CompactInfo = null;
}
private void VerifyCompactInfo()
{
var info = m_CompactInfo;
if (info == null)
{
return;
}
var isValid = info.m_Name != null
|| info.m_Items != null
|| info.m_Bounce != null
|| info.m_HeldBy != null
|| info.m_BlessedFor != null
|| info.m_Spawner != null
|| info.m_TempFlags != 0
|| info.m_SavedFlags != 0
|| info.m_Weight >= 0;
if (!isValid)
{
ReleaseCompactInfo();
}
}
public List<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 (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 &gt;= 100)
/// return true;
///
/// return base.AllowEquippedCast( from );
/// }</code>
/// When placed in an Item script, the item may be cast when equipped if the <paramref name="from" /> has 100 or more
/// intelligence. Otherwise, it will drop to their backpack.
/// </example>
public virtual bool AllowEquippedCast(Mobile from) => false;
public virtual bool CheckConflictingLayer(Mobile m, Item item, Layer layer) => m_Layer == layer;
// Uses Race.RaceFlag
public virtual int RequiredRaces => Race.AllowAllRaces;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CheckRace(Race race) => Race.IsAllowedRace(race, RequiredRaces);
public virtual bool CheckRace(Mobile from, bool message = true)
{
var race = from.Race;
var requiredRaces = RequiredRaces;
if (Race.IsAllowedRace(race, requiredRaces))
{
return true;
}
if (!message)
{
return false;
}
if (requiredRaces == Race.AllowGargoylesOnly)
{
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1111707); // Only gargoyles can wear this.
}
else if (race == Race.Gargoyle)
{
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1111708); // Gargoyles can't wear this.
}
else if (requiredRaces == Race.AllowElvesOnly)
{
from.SendLocalizedMessage(1072203); // Only Elves may use this.
}
else
{
from.SendMessage($"{race.PluralName} may not use this.");
}
return false;
}
public virtual bool CanEquip(Mobile m) => m_Layer != Layer.Invalid && m.FindItemOnLayer(m_Layer) == null;
public virtual void GetChildContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list, Item item)
{
if (m_Parent is Item parentItem)
{
parentItem.GetChildContextMenuEntries(from, ref list, item);
}
else if (m_Parent is Mobile parentMobile)
{
parentMobile.GetChildContextMenuEntries(from, ref list, item);
}
}
public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
{
if (m_Parent is Item item)
{
item.GetChildContextMenuEntries(from, ref list, this);
}
else if (m_Parent is Mobile mobile)
{
mobile.GetChildContextMenuEntries(from, ref list, this);
}
}
public virtual bool VerifyMove(Mobile from) => Movable;
public virtual DeathMoveResult OnParentDeath(Mobile parent)
{
if (!Movable)
{
return DeathMoveResult.RemainEquipped;
}
if (parent.KeepsItemsOnDeath)
{
return DeathMoveResult.MoveToBackpack;
}
if (CheckBlessed(parent))
{
return DeathMoveResult.MoveToBackpack;
}
if (CheckNewbied() && !parent.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 => LastMoved + DecayTime;
public void UpdateDecayRegistration()
{
DecayScheduler.Unregister(this);
if (CanDecay())
{
DecayScheduler.Register(this);
}
}
public void SetLastMoved()
{
LastMoved = Core.Now;
UpdateDecayRegistration();
}
public virtual bool CanStackWith(Item dropped) =>
dropped.Stackable && Stackable &&
dropped.GetType() == GetType() &&
dropped.ItemID == ItemID &&
dropped.Hue == Hue &&
dropped.Name == Name &&
dropped.Amount + Amount <= 60000 &&
dropped != this;
public bool StackWith(Mobile from, Item dropped) => StackWith(from, dropped, true);
public virtual bool StackWith(Mobile from, Item dropped, bool playSound)
{
if (!CanStackWith(dropped))
{
return false;
}
if (m_LootType != dropped.m_LootType)
{
m_LootType = LootType.Regular;
}
Amount += dropped.Amount;
dropped.Delete();
if (playSound && from != null)
{
var soundID = GetDropSound();
if (soundID == -1)
{
soundID = 0x42;
}
from.SendSound(soundID, GetWorldLocation());
}
return true;
}
public virtual bool OnDragDrop(Mobile from, Item dropped)
{
var success = (Parent as Container)?.OnStackAttempt(from, this, dropped) ??
StackWith(from, dropped);
if (success && Spawner != null)
{
Spawner.Remove(this);
Spawner = null;
}
return success;
}
public Rectangle2D GetGraphicBounds()
{
var itemID = m_ItemID;
var doubled = m_Amount > 1;
if (itemID is >= 0xEEA and <= 0xEF2) // Are we coins?
{
var coinBase = (itemID - 0xEEA) / 3;
coinBase *= 3;
coinBase += 0xEEA;
doubled = false;
itemID = m_Amount switch
{
<= 1 => coinBase,
<= 5 => coinBase + 1,
_ => coinBase + 2
};
}
var bounds = ItemBounds.Bounds[itemID & 0x3FFF];
if (doubled)
{
bounds.Set(bounds.X, bounds.Y, bounds.Width + 5, bounds.Height + 5);
}
return bounds;
}
public virtual void AppendChildProperties(IPropertyList list)
{
if (m_Parent is Item item)
{
item.GetChildProperties(list, this);
}
else if (m_Parent is Mobile mobile)
{
mobile.GetChildProperties(list, this);
}
}
public virtual void AppendChildNameProperties(IPropertyList list)
{
if (m_Parent is Item item)
{
item.GetChildNameProperties(list, this);
}
else if (m_Parent is Mobile mobile)
{
mobile.GetChildNameProperties(list, this);
}
}
private ObjectPropertyList InitializePropertyList(ObjectPropertyList list)
{
GetProperties(list);
AppendChildProperties(list);
list.Terminate();
return list;
}
public virtual void ClearProperties()
{
m_PropertyList = null;
}
#nullable enable
public virtual void InvalidateProperties()
{
if (!ObjectPropertyList.Enabled)
{
return;
}
if (m_Map != null && m_Map != Map.Internal && !World.Loading)
{
int? oldHash;
int newHash;
if (m_PropertyList != null)
{
oldHash = m_PropertyList.Hash;
m_PropertyList.Reset();
InitializePropertyList(m_PropertyList);
newHash = m_PropertyList.Hash;
}
else
{
oldHash = null;
newHash = PropertyList.Hash;
}
if (oldHash != newHash)
{
Delta(ItemDelta.Properties);
}
}
else
{
ClearProperties();
}
}
#nullable restore
public virtual int GetPacketFlags()
{
var flags = 0;
if (!Visible)
{
flags |= 0x80;
}
if (Movable || ForceShowProperties)
{
flags |= 0x20;
}
return flags;
}
public virtual bool OnMoveOff(Mobile m) => true;
public virtual bool OnMoveOver(Mobile m) => true;
public virtual void OnMovement(Mobile m, Point3D oldLocation)
{
}
public void Internalize()
{
MoveToWorld(Point3D.Zero, Map.Internal);
}
public virtual void OnMapChange()
{
}
public virtual void OnRemoved(IEntity parent)
{
}
public virtual void OnAdded(IEntity parent)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetSaveFlag(SaveFlag flags, SaveFlag toGet) => (flags & toGet) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Map.ItemAtEnumerable<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 9:
case 8:
case 7:
case 6:
{
var flags = (SaveFlag)reader.ReadInt();
if (version < 7)
{
LastMoved = reader.ReadDeltaTime();
}
else
{
var minutes = reader.ReadEncodedInt();
try
{
LastMoved = Core.Now - TimeSpan.FromMinutes(minutes);
}
catch
{
LastMoved = Core.Now;
}
}
if (GetSaveFlag(flags, SaveFlag.Direction))
{
m_Direction = (Direction)reader.ReadByte();
}
if (GetSaveFlag(flags, SaveFlag.Bounce))
{
AcquireCompactInfo().m_Bounce = BounceInfo.Deserialize(reader);
}
if (GetSaveFlag(flags, SaveFlag.LootType))
{
m_LootType = (LootType)reader.ReadByte();
}
int x = 0, y = 0, z = 0;
if (GetSaveFlag(flags, SaveFlag.LocationFull))
{
x = reader.ReadEncodedInt();
y = reader.ReadEncodedInt();
z = reader.ReadEncodedInt();
}
else
{
if (GetSaveFlag(flags, SaveFlag.LocationByteXY))
{
x = reader.ReadByte();
y = reader.ReadByte();
}
else if (GetSaveFlag(flags, SaveFlag.LocationShortXY))
{
x = reader.ReadShort();
y = reader.ReadShort();
}
if (GetSaveFlag(flags, SaveFlag.LocationSByteZ))
{
z = reader.ReadSByte();
}
}
m_Location = new Point3D(x, y, z);
if (GetSaveFlag(flags, SaveFlag.ItemID))
{
m_ItemID = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, SaveFlag.Hue))
{
m_Hue = reader.ReadEncodedInt();
}
m_Amount = GetSaveFlag(flags, SaveFlag.Amount) ? reader.ReadEncodedInt() : 1;
if (GetSaveFlag(flags, SaveFlag.Layer))
{
m_Layer = (Layer)reader.ReadByte();
}
if (GetSaveFlag(flags, SaveFlag.Name))
{
var name = reader.ReadString();
if (name != DefaultName)
{
AcquireCompactInfo().m_Name = name;
}
}
if (GetSaveFlag(flags, SaveFlag.Parent))
{
var parent = reader.ReadSerial();
if (parent.IsMobile)
{
m_Parent = World.FindMobile(parent);
}
else if (parent.IsItem)
{
m_Parent = World.FindItem(parent);
}
else
{
m_Parent = null;
}
if (m_Parent == null && (parent.IsMobile || parent.IsItem))
{
Timer.DelayCall(Delete);
}
}
if (GetSaveFlag(flags, SaveFlag.Items))
{
var items = reader.ReadEntityList<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();
}
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;
}
public static void ProcessDeltaQueue()
{
var limit = m_DeltaQueue.Count;
while (m_DeltaQueue.Count > 0 && --limit >= 0)
{
var item = m_DeltaQueue.Dequeue();
if (item == null)
{
continue;
}
item.SetFlag(ImplFlag.InQueue, false);
try
{
item.ProcessDelta();
}
catch (Exception ex)
{
logger.Debug(ex, "Process Delta Queue for {Item} failed", item);
}
}
if (m_DeltaQueue.Count > 0)
{
logger.Warning(
"{Count} items left in delta queue after processing.",
m_DeltaQueue.Count
);
}
}
#if TRACK_LEAKS
~Item()
{
EntityFinalizationTracker.NotifyFinalized(this);
}
#endif
public virtual void OnDelete()
{
if (Spawner != null)
{
Spawner.Remove(this);
Spawner = null;
}
}
public virtual void OnParentDeleted(IEntity parent)
{
Delete();
}
public virtual void OnAfterDelete()
{
}
public virtual void RemoveItem(Item item)
{
var items = LookupItems();
if (items.Remove(item))
{
if (this is not Container)
{
AcquireCompactInfo().Version++;
}
item.SendRemovePacket();
if (!item.IsVirtualItem)
{
UpdateTotal(item, TotalType.Gold, -item.TotalGold);
UpdateTotal(item, TotalType.Items, -(item.TotalItems + 1));
UpdateTotal(item, TotalType.Weight, -(item.TotalWeight + item.PileWeight));
}
item.Parent = null;
item.UpdateDecayRegistration();
item.OnRemoved(this);
OnItemRemoved(item);
}
}
public virtual void OnAfterDuped(Item newItem)
{
}
// Warning: This uses reflection and is slow!
public virtual void Dupe(Item newItem)
{
CopyProperties(this, newItem);
OnAfterDuped(newItem);
}
// Warning: This uses reflection and is slow!
public static void CopyProperties(Item src, Item dest)
{
var props = src.GetType().GetProperties();
for (var i = 0; i < props.Length; i++)
{
var p = props[i];
if (p.GetCustomAttribute(typeof(IgnoreDupeAttribute), true) != null || !p.CanRead || !p.CanWrite)
{
continue;
}
var setMethod = p.GetSetMethod(false);
try
{
// Do not copy private properties
if (setMethod != null)
{
p.SetValue(dest, p.GetValue(src, null), null);
}
}
catch
{
// ignored
}
}
}
public virtual bool OnDragLift(Mobile from) => true;
public virtual bool OnEquip(Mobile from) => true;
protected virtual void OnAmountChange(int oldValue)
{
}
public virtual void OnSpeech(SpeechEventArgs e)
{
}
public virtual bool OnDroppedToMobile(Mobile from, Mobile target)
{
if (Nontransferable && from.Player)
{
HandleInvalidTransfer(from);
return false;
}
return true;
}
public virtual bool DropToMobile(Mobile from, Mobile target, Point3D p) =>
!(Deleted || from.Deleted || target.Deleted) && from.Map == target.Map && from.Map != null &&
target.Map != null && (from.AccessLevel >= AccessLevel.GameMaster || from.InRange(target.Location, 2)) &&
from.CanSee(target) && from.InLOS(target) && from.OnDroppedItemToMobile(this, target) &&
OnDroppedToMobile(from, target) && target.OnDragDrop(from, this);
public virtual bool OnDroppedInto(Mobile from, Container target, Point3D p)
{
if (!from.OnDroppedItemInto(this, target, p))
{
return false;
}
if (Nontransferable && from.Player && target != from.Backpack)
{
HandleInvalidTransfer(from);
return false;
}
return target.OnDragDropInto(from, this, p);
}
public virtual bool OnDroppedOnto(Mobile from, Item target)
{
if (Deleted || from.Deleted || target.Deleted || from.Map != target.Map || from.Map == null ||
target.Map == null)
{
return false;
}
if (from.AccessLevel < AccessLevel.GameMaster && !from.InRange(target.GetWorldLocation(), 2))
{
return false;
}
if (!from.CanSee(target) || !from.InLOS(target))
{
return false;
}
if (!target.IsAccessibleTo(from))
{
return false;
}
if (!from.OnDroppedItemOnto(this, target))
{
return false;
}
if (Nontransferable && from.Player && target != from.Backpack)
{
HandleInvalidTransfer(from);
return false;
}
return target.OnDragDrop(from, this);
}
public virtual bool DropToItem(Mobile from, Item target, Point3D p)
{
if (Deleted || from.Deleted || target.Deleted || from.Map != target.Map || from.Map == null ||
target.Map == null)
{
return false;
}
if (from.AccessLevel < AccessLevel.GameMaster && !from.InRange(target.GetWorldLocation(), 2))
{
return false;
}
if (!from.CanSee(target) || !from.InLOS(target))
{
return false;
}
if (!target.IsAccessibleTo(from))
{
return false;
}
if (target.RootParent is Mobile mobile && !mobile.CheckNonlocalDrop(from, this, target))
{
return false;
}
if (!from.OnDroppedItemToItem(this, target, p))
{
return false;
}
if (target is Container container && p.m_X != -1 && p.m_Y != -1)
{
return OnDroppedInto(from, container, p);
}
return OnDroppedOnto(from, target);
}
public virtual bool OnDroppedToWorld(Mobile from, Point3D p)
{
if (Nontransferable && from.Player)
{
HandleInvalidTransfer(from);
return false;
}
return true;
}
public virtual int GetLiftSound(Mobile from) => 0x57;
public virtual bool DropToWorld(Mobile from, Point3D p)
{
if (Deleted || from.Deleted || from.Map == null)
{
return false;
}
if (!from.InRange(p, 2))
{
return false;
}
var map = from.Map;
if (map == null)
{
return false;
}
int x = p.m_X, y = p.m_Y;
var z = int.MinValue;
var maxZ = from.Z + 16;
var landTile = map.Tiles.GetLandTile(x, y);
var landFlags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags;
map.GetAverageZ(x, y, out var landZ, out var landAvg, out _);
if (!landTile.Ignored && (landFlags & TileFlag.Impassable) == 0)
{
if (landAvg <= maxZ)
{
z = landAvg;
}
}
foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y))
{
var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue];
if (!id.Surface)
{
continue;
}
var top = tile.Z + id.CalcHeight;
if (top > maxZ || top < z)
{
continue;
}
z = top;
}
using var items = PooledRefList<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 || Mobile.InsuranceEnabled && Insured || m != null && m == BlessedFor;
public virtual bool CheckNewbied() => m_LootType == LootType.Newbied;
public virtual bool IsStandardLoot() =>
(!Mobile.InsuranceEnabled || !Insured) && BlessedFor == null && m_LootType == LootType.Regular;
public override string ToString() => $"{Serial} \"{GetType().Name}\"";
public virtual void OnSectorActivate()
{
}
public virtual void OnSectorDeactivate()
{
}
public virtual void OnLocationChange(Point3D oldLocation)
{
}
public virtual void OnDoubleClick(Mobile from)
{
}
public virtual void OnDoubleClickOutOfRange(Mobile from)
{
}
public virtual void OnDoubleClickCantSee(Mobile from)
{
}
public virtual void OnDoubleClickDead(Mobile from)
{
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019048); // I am dead and cannot do that.
}
public virtual void OnDoubleClickNotAccessible(Mobile from)
{
from.SendLocalizedMessage(500447); // That is not accessible.
}
public virtual void OnDoubleClickSecureTrade(Mobile from)
{
from.SendLocalizedMessage(500447); // That is not accessible.
}
[Flags]
private enum ImplFlag : byte
{
None = 0x00,
Visible = 0x01,
Movable = 0x02,
Deleted = 0x04,
Stackable = 0x08,
InQueue = 0x10,
Insured = 0x20,
PaidInsurance = 0x40,
QuestItem = 0x80
}
private class CompactInfo
{
public Mobile m_BlessedFor;
public BounceInfo m_Bounce;
public Mobile m_HeldBy;
public List<Item> m_Items;
public string m_Name;
public int m_SavedFlags;
public ISpawner m_Spawner;
public int m_TempFlags;
public double m_Weight = -1;
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
}
}