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(); - } }