diff --git a/Projects/Server.Tests/Fixtures/TestServerInitializer.cs b/Projects/Server.Tests/Fixtures/TestServerInitializer.cs
index ef5a9864f..a6aca8244 100644
--- a/Projects/Server.Tests/Fixtures/TestServerInitializer.cs
+++ b/Projects/Server.Tests/Fixtures/TestServerInitializer.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Reflection;
using System.Threading;
+using Server.Items;
namespace Server.Tests;
@@ -95,6 +96,8 @@ public static class TestServerInitializer
DetectTileIds();
}
+ DecayScheduler.Configure();
+
_initialized = true;
}
}
diff --git a/Projects/Server/Items/DecayScheduler.cs b/Projects/Server/Items/DecayScheduler.cs
new file mode 100644
index 000000000..73ba98f66
--- /dev/null
+++ b/Projects/Server/Items/DecayScheduler.cs
@@ -0,0 +1,338 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2025 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: DecayScheduler.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Collections.Generic;
+
+namespace Server.Items;
+
+///
+/// Timer wheel-based decay scheduler with O(1) registration/unregistration.
+/// Uses coarse-grained HashSet buckets for time intervals, with a PriorityQueue
+/// for fine-grained processing of items due within the current window.
+///
+public class DecayScheduler : Timer
+{
+ private const int BucketCount = 12;
+ private static int _maxItemsPerTick;
+ private static TimeSpan _tickInterval;
+ private static TimeSpan _bucketInterval;
+ private static int _jitterMaxMilliseconds;
+
+ // Timer wheel buckets (HashSets for O(1) add/remove)
+ private static HashSet- [] _buckets;
+ private static int _currentBucketIndex;
+ private static DateTime _nextBucketRotation;
+
+ // Overflow bucket (separate from regular rotation, for items with decay > total bucket span)
+ private static HashSet
- _overflowBucket;
+ private static TimeSpan _totalBucketSpan;
+ private static DateTime _nextOverflowCheck;
+
+ // Active processing queue for items due within current bucket window
+ private static readonly PriorityQueue
- _activeQueue = new();
+
+ public static DecayScheduler Shared { get; private set; }
+
+ public static void Configure()
+ {
+ _maxItemsPerTick = ServerConfiguration.GetSetting("decay.maxItemsPerTick", 250);
+ _tickInterval = ServerConfiguration.GetSetting("decay.tickInterval", TimeSpan.FromMilliseconds(256));
+ _bucketInterval = ServerConfiguration.GetSetting("decay.bucketInterval", TimeSpan.FromMinutes(5));
+ _jitterMaxMilliseconds = ServerConfiguration.GetSetting("decay.jitterMaxMs", 25); // ±25ms jitter
+
+ _buckets = new HashSet
- [BucketCount];
+ for (var i = 0; i < BucketCount; i++)
+ {
+ _buckets[i] = [];
+ }
+
+ // Overflow bucket is separate from the timer wheel
+ _overflowBucket = [];
+ _totalBucketSpan = TimeSpan.FromTicks(_bucketInterval.Ticks * BucketCount);
+
+ var now = Core.Now;
+ _nextBucketRotation = now + _bucketInterval;
+ _nextOverflowCheck = now + _totalBucketSpan;
+
+ Shared = new DecayScheduler();
+ }
+
+ private DecayScheduler() : base(_tickInterval, _tickInterval)
+ {
+ }
+
+ ///
+ /// Checks if all decay tracking structures are empty.
+ ///
+ private static bool IsEmpty()
+ {
+ if (_activeQueue.Count > 0 || _overflowBucket.Count > 0)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < BucketCount; i++)
+ {
+ if (_buckets[i].Count > 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Registers an item for decay tracking. Call after item becomes decay-eligible.
+ ///
+ public static void Register(Item item)
+ {
+ if (item?.Deleted != false || !item.CanDecay())
+ {
+ return;
+ }
+
+ // Start timer if not running
+ Shared.Start();
+
+ var decayTime = item.ScheduledDecayTime;
+ var timeUntilDecay = decayTime - Core.Now;
+
+ // If already due or due very soon, add directly to active queue
+ if (timeUntilDecay <= _bucketInterval)
+ {
+ _activeQueue.Enqueue(item, decayTime);
+ }
+ // If beyond the total bucket span, add to overflow bucket
+ else if (timeUntilDecay > _totalBucketSpan)
+ {
+ _overflowBucket.Add(item);
+ }
+ else
+ {
+ // Calculate bucket index relative to current position
+ var bucketOffset = GetBucketOffset(timeUntilDecay);
+ var absoluteIndex = (_currentBucketIndex + bucketOffset) % BucketCount;
+ _buckets[absoluteIndex].Add(item);
+ }
+ }
+
+ ///
+ /// Unregisters an item from decay tracking. Call when item is picked up, deleted, or otherwise ineligible.
+ ///
+ public static void Unregister(Item item)
+ {
+ if (item?.Deleted != false)
+ {
+ return;
+ }
+
+ // Check overflow bucket first
+ if (_overflowBucket.Remove(item))
+ {
+ return;
+ }
+
+ // Check regular buckets
+ for (var i = 0; i < BucketCount; i++)
+ {
+ if (_buckets[i].Remove(item))
+ {
+ return;
+ }
+ }
+
+ _activeQueue.Remove(item, out _, out _);
+ }
+
+ ///
+ /// Gets the bucket offset for an item based on time until decay.
+ /// Returns a value from 0 to BucketCount - 1, representing how many buckets ahead of current.
+ ///
+ private static int GetBucketOffset(TimeSpan timeUntilDecay)
+ {
+ // Items due within _bucketInterval go to active queue, so offset 0 means _bucketInterval to 2*_bucketInterval
+ var bucketOffset = (int)(timeUntilDecay.Ticks / _bucketInterval.Ticks) - 1;
+
+ // Clamp to valid range within regular buckets
+ return Math.Clamp(bucketOffset, 0, BucketCount - 1);
+ }
+
+ protected override void OnTick()
+ {
+ var now = Core.Now;
+
+ // Process items from active queue first (smaller queue = faster log(n) operations)
+ ProcessActiveQueue(now);
+
+ // Then check if it's time to rotate buckets (adds items for next tick)
+ if (now >= _nextBucketRotation)
+ {
+ RotateBuckets(now);
+ }
+
+ // Check overflow bucket on its own schedule (when items might enter the regular window)
+ if (now >= _nextOverflowCheck)
+ {
+ ProcessOverflow(now);
+ }
+
+ // Stop timer if nothing left to track
+ if (IsEmpty())
+ {
+ Stop();
+ return;
+ }
+
+ // Apply jitter for next tick to prevent synchronization with other systems
+ if (_jitterMaxMilliseconds > 0)
+ {
+ var jitter = Utility.Random(-_jitterMaxMilliseconds, _jitterMaxMilliseconds * 2 + 1);
+ Interval = _tickInterval + TimeSpan.FromMilliseconds(jitter);
+ }
+ }
+
+ ///
+ /// Rotates the timer wheel, moving the next bucket's contents into the active queue.
+ ///
+ private static void RotateBuckets(DateTime now)
+ {
+ _nextBucketRotation = now + _bucketInterval;
+
+ // Get the next bucket to process
+ var bucket = _buckets[_currentBucketIndex];
+
+ // Move all items from this bucket into the active queue (with validation)
+ foreach (var item in bucket)
+ {
+ if (item.Deleted || !item.CanDecay())
+ {
+ continue; // Lazy cleanup - item was picked up or deleted
+ }
+
+ var decayTime = item.ScheduledDecayTime;
+ var timeUntilDecay = decayTime - now;
+
+ if (timeUntilDecay > _bucketInterval)
+ {
+ // Item was moved (SetLastMoved called) - re-bucket or move to overflow
+ if (timeUntilDecay > _totalBucketSpan)
+ {
+ // Extended beyond total span - move to overflow
+ _overflowBucket.Add(item);
+ }
+ else
+ {
+ // Re-bucket within regular buckets
+ var bucketOffset = GetBucketOffset(timeUntilDecay);
+ var actualIndex = (_currentBucketIndex + bucketOffset) % BucketCount;
+
+ if (actualIndex != _currentBucketIndex)
+ {
+ _buckets[actualIndex].Add(item);
+ }
+ else
+ {
+ // Still maps to current bucket - add to active queue
+ _activeQueue.Enqueue(item, decayTime);
+ }
+ }
+ continue;
+ }
+
+ // Due within next window - add to active queue
+ _activeQueue.Enqueue(item, decayTime);
+ }
+
+ // Clear the processed bucket
+ bucket.Clear();
+
+ // Advance to next bucket
+ _currentBucketIndex = (_currentBucketIndex + 1) % BucketCount;
+ }
+
+ ///
+ /// Processes the overflow bucket, moving items that are now within the regular bucket window.
+ ///
+ private static void ProcessOverflow(DateTime now)
+ {
+ _nextOverflowCheck = now + _totalBucketSpan;
+
+ // Move items that are now within the regular bucket span
+ _overflowBucket.RemoveWhere(item =>
+ {
+ if (item.Deleted || !item.CanDecay())
+ {
+ return true; // Remove invalid items
+ }
+
+ var timeUntilDecay = item.ScheduledDecayTime - now;
+
+ if (timeUntilDecay > _totalBucketSpan)
+ {
+ return false; // Keep in overflow
+ }
+
+ // Now within regular bucket range - move to appropriate bucket
+ if (timeUntilDecay <= _bucketInterval)
+ {
+ _activeQueue.Enqueue(item, item.ScheduledDecayTime);
+ }
+ else
+ {
+ var bucketOffset = GetBucketOffset(timeUntilDecay);
+ var actualIndex = (_currentBucketIndex + bucketOffset) % BucketCount;
+ _buckets[actualIndex].Add(item);
+ }
+
+ return true; // Remove from overflow
+ });
+ }
+
+ ///
+ /// Processes items from the active queue that are due for decay.
+ ///
+ private static void ProcessActiveQueue(DateTime now)
+ {
+ var processed = 0;
+
+ while (_activeQueue.TryPeek(out var item, out var scheduledTime) && processed < _maxItemsPerTick)
+ {
+ // Not yet due - stop processing
+ if (scheduledTime > now)
+ {
+ break;
+ }
+
+ processed++;
+ _activeQueue.Dequeue();
+
+ if (item.Deleted || !item.CanDecay())
+ {
+ continue;
+ }
+
+ if (item.ScheduledDecayTime > now)
+ {
+ Register(item);
+ }
+ else if (item.OnDecay())
+ {
+ item.Delete();
+ }
+ }
+ }
+}
diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs
index da4b57d85..bfebd5e87 100644
--- a/Projects/Server/Items/Item.cs
+++ b/Projects/Server/Items/Item.cs
@@ -368,6 +368,7 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
}
Delta(ItemDelta.Update);
+ UpdateDecayRegistration();
}
}
}
@@ -383,6 +384,7 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
SetFlag(ImplFlag.Movable, value);
Delta(ItemDelta.Update);
+ UpdateDecayRegistration();
}
}
}
@@ -1436,6 +1438,7 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
}
OnDelete();
+ DecayScheduler.Unregister(this);
var items = LookupItems();
@@ -1478,6 +1481,8 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
ClearProperties();
}
+ public virtual bool SkipSerialization => false;
+
[IgnoreDupe]
public ISpawner Spawner
{
@@ -1485,6 +1490,7 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
set
{
var info = AcquireCompactInfo();
+ var oldValue = info.m_Spawner;
info.m_Spawner = value;
@@ -1492,6 +1498,11 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
{
VerifyCompactInfo();
}
+
+ if (oldValue != value)
+ {
+ UpdateDecayRegistration();
+ }
}
}
@@ -2268,9 +2279,22 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
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) =>
@@ -3205,6 +3229,7 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
}
item.Delta(ItemDelta.Update);
+ item.UpdateDecayRegistration();
item.OnAdded(this);
OnItemAdded(item);
@@ -3374,6 +3399,7 @@ public partial class Item : IHued, IComparable
- , ISpawnable, IObjectPropert
}
item.Parent = null;
+ item.UpdateDecayRegistration();
item.OnRemoved(this);
OnItemRemoved(item);
diff --git a/Projects/Server/Items/VirtualCheck.cs b/Projects/Server/Items/VirtualCheck.cs
index 424a04eae..ae2b35bfe 100644
--- a/Projects/Server/Items/VirtualCheck.cs
+++ b/Projects/Server/Items/VirtualCheck.cs
@@ -47,6 +47,7 @@ public sealed partial class VirtualCheck : Item
Movable = false;
}
+ public override bool SkipSerialization => true;
public override bool IsVirtualItem => true;
public override bool DisplayWeight => false;
public override bool DisplayLootType => false;
@@ -158,10 +159,4 @@ public sealed partial class VirtualCheck : Item
Editor = null;
}
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index 93301c32e..02e4dbbc2 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -2458,6 +2458,8 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
m_PropertyList = null;
}
+ public virtual bool SkipSerialization => false;
+
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public Map Map
{
diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs
index e2a25909c..262d000aa 100644
--- a/Projects/Server/Serialization/GenericEntityPersistence.cs
+++ b/Projects/Server/Serialization/GenericEntityPersistence.cs
@@ -98,10 +98,19 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer
}
idx.Write(3); // Version
- idx.Write(EntitiesBySerial.Values.Count);
+ var countPosition = idx.Position;
+ idx.Write(0);
+
+ var entityCount = EntitiesBySerial.Count;
foreach (var e in EntitiesBySerial.Values)
{
+ if (e is Item { SkipSerialization: true } or Mobile { SkipSerialization: true })
+ {
+ entityCount--;
+ continue;
+ }
+
var thread = e.SerializedThread;
var heapStart = e.SerializedPosition;
var heapLength = e.SerializedLength;
@@ -130,6 +139,11 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer
binPosition += heapLength;
}
+
+ var currentPosition = idx.Position;
+ idx.Seek(countPosition, SeekOrigin.Begin);
+ idx.Write(entityCount);
+ idx.Seek(currentPosition, SeekOrigin.Begin);
}
public override void Serialize()
diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs
index 4d1c4a23d..0d9e2aeb6 100644
--- a/Projects/Server/World/World.cs
+++ b/Projects/Server/World/World.cs
@@ -47,7 +47,6 @@ public static class World
private static int _threadId;
internal static SerializationThreadWorker[] _threadWorkers;
private static readonly ManualResetEvent _diskWriteHandle = new(true);
- private static readonly ConcurrentQueue
- _decayQueue = new();
private static string _tempSavePath; // Path to the temporary folder for the save
@@ -109,18 +108,6 @@ public static class World
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WaitForWriteCompletion() => _diskWriteHandle.WaitOne();
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void EnqueueForDecay(Item item)
- {
- if (WorldState != WorldState.Saving)
- {
- logger.Warning("Attempting to queue {Item} for decay but the world is not saving", item);
- return;
- }
-
- _decayQueue.Enqueue(item);
- }
-
public static void Broadcast(int hue, bool ascii, string text)
{
var length = OutgoingMessagePackets.GetMaxMessageLength(text);
@@ -216,20 +203,6 @@ public static class World
}
}
- private static void ProcessDecay()
- {
- while (_decayQueue.TryDequeue(out var item))
- {
- if (item.OnDecay())
- {
- // TODO: Add Logging
- item.Delete();
- }
- }
- }
-
- private static DateTime _serializationStart;
-
/**
* Duplicates can be weeded out asynchronously while flushing
* If performance becomes a problem, we need to build a dual mode concurrent array.
@@ -322,8 +295,6 @@ public static class World
try
{
- _serializationStart = Core.Now;
-
if (string.IsNullOrEmpty(snapshotPath))
{
throw new ArgumentException("Snapshot path cannot be null or empty", nameof(snapshotPath));
@@ -543,28 +514,9 @@ public static class World
}
item.ClearProperties();
+ item.UpdateDecayRegistration();
}
}
-
- public override void Serialize()
- {
- foreach (var item in EntitiesBySerial.Values)
- {
- if (item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart)
- {
- EnqueueForDecay(item);
- }
-
- PushToCache(item);
- }
- }
-
- public override void PostWorldSave()
- {
- ProcessDecay(); // Run this before the safety queue
-
- base.PostWorldSave();
- }
}
private class MobilePersistence : GenericEntityPersistence
diff --git a/Projects/UOContent.Tests/Fixtures/UOContentFixture.cs b/Projects/UOContent.Tests/Fixtures/UOContentFixture.cs
index 658bbb9f9..d8c77e138 100644
--- a/Projects/UOContent.Tests/Fixtures/UOContentFixture.cs
+++ b/Projects/UOContent.Tests/Fixtures/UOContentFixture.cs
@@ -1,5 +1,6 @@
using System;
using System.Reflection;
+using Server.Items;
using Server.Misc;
using Xunit;
@@ -39,6 +40,8 @@ public class UOContentFixture : ICollectionFixture, IDisposabl
World.Load();
World.ExitSerializationThreads();
+
+ DecayScheduler.Configure();
}
private static int _counter;
diff --git a/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs b/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs
index 71311ba2d..d1f848f0f 100644
--- a/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs
+++ b/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs
@@ -37,11 +37,13 @@ namespace Server.Engines.ConPVP
{
}
+ public override bool SkipSerialization => true;
+
public override string DefaultName => "da bomb";
public Mobile Thrower { get; private set; }
- private Mobile FindOwner(IEntity parent)
+ private static Mobile FindOwner(IEntity parent)
{
if (parent is Item item)
{
@@ -61,16 +63,6 @@ namespace Server.Engines.ConPVP
base.Deserialize(reader);
var version = reader.ReadInt();
-
- switch (version)
- {
- case 0:
- {
- break;
- }
- }
-
- Timer.StartTimer(Delete); // delete this after the world loads
}
public override void Serialize(IGenericWriter writer)
diff --git a/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs b/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs
index 90e8cc1ea..efae770d7 100644
--- a/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs
+++ b/Projects/UOContent/Engines/Plants/MiscItems/GreenThorns.cs
@@ -703,6 +703,8 @@ public partial class GreenThornsSHTeleporter : Item
public override string DefaultName => "a hole";
+ public override bool SkipSerialization => true;
+
public static void Create(Point3D location, Map map)
{
var tele = new GreenThornsSHTeleporter();
@@ -726,12 +728,6 @@ public partial class GreenThornsSHTeleporter : Item
}
}
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
-
private class InternalTimer : Timer
{
private readonly GreenThornsSHTeleporter _teleporter;
diff --git a/Projects/UOContent/Engines/Quests/Core/Items/HornOfRetreat.cs b/Projects/UOContent/Engines/Quests/Core/Items/HornOfRetreat.cs
index 2fdfe7146..b63196c8f 100644
--- a/Projects/UOContent/Engines/Quests/Core/Items/HornOfRetreat.cs
+++ b/Projects/UOContent/Engines/Quests/Core/Items/HornOfRetreat.cs
@@ -115,6 +115,8 @@ public partial class HornOfRetreatMoongate : Moongate
public override int LabelNumber => 1049114; // Sanctuary Gate
+ public override bool SkipSerialization => true;
+
public override void BeginConfirmation(Mobile from)
{
EndConfirmation(from);
@@ -132,10 +134,4 @@ public partial class HornOfRetreatMoongate : Moongate
Delete();
}
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
diff --git a/Projects/UOContent/Engines/Quests/Witch Apprentice/Mobiles/Zeefzorpul.cs b/Projects/UOContent/Engines/Quests/Witch Apprentice/Mobiles/Zeefzorpul.cs
index b1db0e4e1..33a3fc42f 100644
--- a/Projects/UOContent/Engines/Quests/Witch Apprentice/Mobiles/Zeefzorpul.cs
+++ b/Projects/UOContent/Engines/Quests/Witch Apprentice/Mobiles/Zeefzorpul.cs
@@ -12,6 +12,8 @@ public partial class Zeefzorpul : BaseQuester
public override string DefaultName => "Zeefzorpul";
+ public override bool SkipSerialization => true;
+
public override void InitBody()
{
Body = 0x4A;
@@ -22,10 +24,4 @@ public partial class Zeefzorpul : BaseQuester
public override void OnTalk(PlayerMobile player, bool contextMenu)
{
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
diff --git a/Projects/UOContent/Engines/Spawners/Commands/ShowSpawnerBordersCommand.cs b/Projects/UOContent/Engines/Spawners/Commands/ShowSpawnerBordersCommand.cs
index fe7b6693d..6c59ac4f6 100644
--- a/Projects/UOContent/Engines/Spawners/Commands/ShowSpawnerBordersCommand.cs
+++ b/Projects/UOContent/Engines/Spawners/Commands/ShowSpawnerBordersCommand.cs
@@ -160,11 +160,7 @@ public partial class SpawnerBorder : ProjectedItem
MinimumVisible = AccessLevel.Developer;
}
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
+ public override bool SkipSerialization => true;
protected override bool SendEffect()
{
diff --git a/Projects/UOContent/Items/Maps/TreasureMap.cs b/Projects/UOContent/Items/Maps/TreasureMap.cs
index e8c853dd6..25711e120 100644
--- a/Projects/UOContent/Items/Maps/TreasureMap.cs
+++ b/Projects/UOContent/Items/Maps/TreasureMap.cs
@@ -901,6 +901,8 @@ public partial class TreasureChestDirt : Item
Timer.StartTimer(TimeSpan.FromMinutes(2.0), Delete);
}
+ public override bool SkipSerialization => true;
+
public void Turn1()
{
ItemID = 0x913;
@@ -910,10 +912,4 @@ public partial class TreasureChestDirt : Item
{
ItemID = 0x914;
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
diff --git a/Projects/UOContent/Items/Misc/Acid.cs b/Projects/UOContent/Items/Misc/Acid.cs
index 4f730610d..400df0ee1 100644
--- a/Projects/UOContent/Items/Misc/Acid.cs
+++ b/Projects/UOContent/Items/Misc/Acid.cs
@@ -39,6 +39,8 @@ public partial class Acid : Item
public override string DefaultName => "a pool of acid";
+ public override bool SkipSerialization => true;
+
public override void OnDelete()
{
_timerToken.Cancel();
@@ -89,10 +91,4 @@ public partial class Acid : Item
{
m.Damage(Utility.RandomMinMax(_minDamage, _maxDamage));
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
diff --git a/Projects/UOContent/Items/Misc/EffectItem.cs b/Projects/UOContent/Items/Misc/EffectItem.cs
index 76c72d420..8cc1b7db5 100644
--- a/Projects/UOContent/Items/Misc/EffectItem.cs
+++ b/Projects/UOContent/Items/Misc/EffectItem.cs
@@ -15,6 +15,8 @@ public partial class EffectItem : Item
public override bool Decays => true;
+ public override bool SkipSerialization => true;
+
public static EffectItem Create(Point3D p, Map map, TimeSpan duration)
{
EffectItem item = null;
@@ -46,12 +48,6 @@ public partial class EffectItem : Item
public void BeginFree(TimeSpan duration) => new FreeTimer(this, duration).Start();
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
-
private class FreeTimer : Timer
{
private EffectItem _item;
diff --git a/Projects/UOContent/Items/Misc/MoonstoneGate.cs b/Projects/UOContent/Items/Misc/MoonstoneGate.cs
index b0eb1d407..a22182402 100644
--- a/Projects/UOContent/Items/Misc/MoonstoneGate.cs
+++ b/Projects/UOContent/Items/Misc/MoonstoneGate.cs
@@ -21,6 +21,8 @@ public partial class MoonstoneGate : Moongate
Effects.PlaySound(loc, map, 0x20E);
}
+ public override bool SkipSerialization => true;
+
public override void CheckGate(Mobile m, int range)
{
if (m.Kills >= 5)
@@ -53,12 +55,6 @@ public partial class MoonstoneGate : Moongate
}
}
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
-
private class InternalTimer : Timer
{
private Item _item;
diff --git a/Projects/UOContent/Items/Skill Items/Camping/Campfire.cs b/Projects/UOContent/Items/Skill Items/Camping/Campfire.cs
index c45962a3a..33f82a2ab 100644
--- a/Projects/UOContent/Items/Skill Items/Camping/Campfire.cs
+++ b/Projects/UOContent/Items/Skill Items/Camping/Campfire.cs
@@ -32,6 +32,8 @@ public partial class Campfire : Item
Timer.StartTimer(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0), OnTick, out _timerToken);
}
+ public override bool SkipSerialization => true;
+
[CommandProperty(AccessLevel.GameMaster)]
public CampfireStatus Status
{
@@ -159,12 +161,6 @@ public partial class Campfire : Item
_timerToken.Cancel();
ClearEntries();
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
public class CampfireEntry
diff --git a/Projects/UOContent/Items/Weapons/Fists.cs b/Projects/UOContent/Items/Weapons/Fists.cs
index c041b5c82..fc4dd2584 100644
--- a/Projects/UOContent/Items/Weapons/Fists.cs
+++ b/Projects/UOContent/Items/Weapons/Fists.cs
@@ -14,6 +14,8 @@ namespace Server.Items
Quality = WeaponQuality.Regular;
}
+ public override bool SkipSerialization => true;
+
public override WeaponAbility PrimaryAbility => WeaponAbility.Disarm;
public override WeaponAbility SecondaryAbility => WeaponAbility.ParalyzingBlow;
@@ -169,12 +171,6 @@ namespace Server.Items
base.PlaySwingAnimation( attacker );
}*/
- [AfterDeserialization(false)]
- private void OnAfterDeserialization()
- {
- Delete();
- }
-
/* Wrestling moves */
private static bool CheckMove(Mobile m, SkillName other)
diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs
index 941301298..ff9f1a800 100644
--- a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs
+++ b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs
@@ -34,13 +34,9 @@ internal sealed partial class TransferItem : Item
Hue = creature.Hue & 0x0FFF;
}
- public static bool IsInCombat(BaseCreature creature) => creature?.Aggressors.Count > 0 || creature?.Aggressed.Count > 0;
+ public override bool SkipSerialization => true;
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
+ public static bool IsInCombat(BaseCreature creature) => creature?.Aggressors.Count > 0 || creature?.Aggressed.Count > 0;
public override void GetProperties(IPropertyList list)
{
diff --git a/Projects/UOContent/Mobiles/Monsters/AOS/Revenant.cs b/Projects/UOContent/Mobiles/Monsters/AOS/Revenant.cs
index 44105e9e5..ae0829753 100644
--- a/Projects/UOContent/Mobiles/Monsters/AOS/Revenant.cs
+++ b/Projects/UOContent/Mobiles/Monsters/AOS/Revenant.cs
@@ -57,6 +57,8 @@ namespace Server.Mobiles
AddItem(new Halberd { Hue = 1, Movable = false });
}
+ public override bool SkipSerialization => true;
+
public override Mobile ConstantFocus => m_Target;
public override bool NoHouseRestrictions => true;
@@ -172,11 +174,5 @@ namespace Server.Mobiles
Delete();
return false;
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
}
diff --git a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs
index f8e3ea8a7..269f244fa 100644
--- a/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs
+++ b/Projects/UOContent/Mobiles/Monsters/Humanoid/Melee/KhaldunRevenant.cs
@@ -56,6 +56,8 @@ namespace Server.Mobiles
AddItem(weapon);
}
+ public override bool SkipSerialization => true;
+
public override bool DeleteCorpseOnDeath => true;
public override Mobile ConstantFocus => m_Target;
@@ -149,11 +151,5 @@ namespace Server.Mobiles
base.OnDelete();
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
}
diff --git a/Projects/UOContent/Mobiles/Monsters/Mammal/Melee/VorpalBunny.cs b/Projects/UOContent/Mobiles/Monsters/Mammal/Melee/VorpalBunny.cs
index b6adac231..732a232e3 100644
--- a/Projects/UOContent/Mobiles/Monsters/Mammal/Melee/VorpalBunny.cs
+++ b/Projects/UOContent/Mobiles/Monsters/Mammal/Melee/VorpalBunny.cs
@@ -102,11 +102,7 @@ namespace Server.Mobiles
public override string DefaultName => "a mysterious rabbit hole";
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
+ public override bool SkipSerialization => true;
}
}
}
diff --git a/Projects/UOContent/Multis/Houses/BaseHouse.cs b/Projects/UOContent/Multis/Houses/BaseHouse.cs
index 358609577..325ec479d 100644
--- a/Projects/UOContent/Multis/Houses/BaseHouse.cs
+++ b/Projects/UOContent/Multis/Houses/BaseHouse.cs
@@ -3739,6 +3739,8 @@ namespace Server.Multis
Movable = false;
}
+ public override bool SkipSerialization => true;
+
public override string DefaultName => "a house transfer contract";
public override void GetProperties(IPropertyList list)
@@ -3775,12 +3777,6 @@ namespace Server.Multis
}
}
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
-
public override bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted)
{
if (!base.AllowSecureTrade(from, to, newOwner, accepted))
diff --git a/Projects/UOContent/Multis/Houses/PreviewHouse.cs b/Projects/UOContent/Multis/Houses/PreviewHouse.cs
index ca254c149..5043709df 100644
--- a/Projects/UOContent/Multis/Houses/PreviewHouse.cs
+++ b/Projects/UOContent/Multis/Houses/PreviewHouse.cs
@@ -35,6 +35,8 @@ public partial class PreviewHouse : BaseMulti
_timer.Start();
}
+ public override bool SkipSerialization => true;
+
public override void OnLocationChange(Point3D oldLocation)
{
base.OnLocationChange(oldLocation);
@@ -94,12 +96,6 @@ public partial class PreviewHouse : BaseMulti
base.OnAfterDelete();
}
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
-
private class DecayTimer : Timer
{
private readonly Item _item;
diff --git a/Projects/UOContent/Spells/Seventh/GateTravel.cs b/Projects/UOContent/Spells/Seventh/GateTravel.cs
index c0cd6b4a8..f35a6a157 100644
--- a/Projects/UOContent/Spells/Seventh/GateTravel.cs
+++ b/Projects/UOContent/Spells/Seventh/GateTravel.cs
@@ -170,6 +170,8 @@ public partial class GateTravelMoongate : Moongate
Timer.StartTimer(TimeSpan.FromSeconds(Core.T2A ? 30.0 : 10.0), Delete);
}
+ public override bool SkipSerialization => true;
+
[CommandProperty(AccessLevel.GameMaster)]
public Moongate LinkedGate { get; set; }
@@ -200,10 +202,4 @@ public partial class GateTravelMoongate : Moongate
base.UseGate(m);
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}
diff --git a/Projects/UOContent/Spells/Spellweaving/Mobiles/NatureFury.cs b/Projects/UOContent/Spells/Spellweaving/Mobiles/NatureFury.cs
index a1deba44f..e1d3316b1 100644
--- a/Projects/UOContent/Spells/Spellweaving/Mobiles/NatureFury.cs
+++ b/Projects/UOContent/Spells/Spellweaving/Mobiles/NatureFury.cs
@@ -36,6 +36,8 @@ public partial class NatureFury : BaseCreature
ControlSlots = 1;
}
+ public override bool SkipSerialization => true;
+
public override bool DeleteCorpseOnDeath => Core.AOS;
public override bool IsHouseSummonable => true;
@@ -65,10 +67,4 @@ public partial class NatureFury : BaseCreature
Timer.StartTimer(TimeSpan.FromSeconds(7.0), DoEffects);
}
}
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- Delete();
- }
}