feat: Adds new decay system and SkipSerialization (#2311)
## Summary - Replaces scan-based decay checking during world saves with an event-driven timer wheel scheduler - Removes virtual property checks from serialization hot path, achieving 6-8x faster world saves ## Changes ### DecayScheduler (new): - Timer wheel with 12 HashSet buckets (5-min intervals) + PriorityQueue for active processing - O(1) register/unregister vs O(n) scan of all items - Auto start/stop when items exist/empty - Configurable tick interval with jitter to prevent system synchronization ### Item.cs: - Added ScheduledDecayTime computed property - Added UpdateDecayRegistration() called from SetLastMoved(), property setters, AddItem()/RemoveItem() - Hooks in Visible, Movable, Spawner setters and Delete() ### World.cs: - Removed _decayQueue, EnqueueForDecay(), ProcessDecay() - ItemPersistence.Serialize() now tight loop without virtual calls ## Performance | Metric | Before | After | Improvement | |---------------|------------|-----------|-------------| | Items (230K) | 245K ticks | 34K ticks | 8x faster | | Mobiles (43K) | 56K ticks | 6K ticks | 9x faster | | Total | 302K ticks | 40K ticks | 7.5x faster | ## Configuration decay.maxItemsPerTick = 250 # Items processed per tick decay.tickInterval = 256ms # Base processing interval decay.bucketInterval = 5min # Timer wheel bucket size decay.jitterMaxMs = 25 # ±25ms tick jitter
This commit is contained in:
parent
c14f361de1
commit
ee2cd1d18d
27 changed files with 426 additions and 173 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
338
Projects/Server/Items/DecayScheduler.cs
Normal file
338
Projects/Server/Items/DecayScheduler.cs
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Item>[] _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<Item> _overflowBucket;
|
||||
private static TimeSpan _totalBucketSpan;
|
||||
private static DateTime _nextOverflowCheck;
|
||||
|
||||
// Active processing queue for items due within current bucket window
|
||||
private static readonly PriorityQueue<Item, DateTime> _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<Item>[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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all decay tracking structures are empty.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an item for decay tracking. Call after item becomes decay-eligible.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters an item from decay tracking. Call when item is picked up, deleted, or otherwise ineligible.
|
||||
/// </summary>
|
||||
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 _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the timer wheel, moving the next bucket's contents into the active queue.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the overflow bucket, moving items that are now within the regular bucket window.
|
||||
/// </summary>
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes items from the active queue that are due for decay.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +368,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
|
||||
Delta(ItemDelta.Update);
|
||||
UpdateDecayRegistration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -383,6 +384,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
SetFlag(ImplFlag.Movable, value);
|
||||
|
||||
Delta(ItemDelta.Update);
|
||||
UpdateDecayRegistration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1436,6 +1438,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
|
||||
OnDelete();
|
||||
DecayScheduler.Unregister(this);
|
||||
|
||||
var items = LookupItems();
|
||||
|
||||
|
|
@ -1478,6 +1481,8 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
ClearProperties();
|
||||
}
|
||||
|
||||
public virtual bool SkipSerialization => false;
|
||||
|
||||
[IgnoreDupe]
|
||||
public ISpawner Spawner
|
||||
{
|
||||
|
|
@ -1485,6 +1490,7 @@ public partial class Item : IHued, IComparable<Item>, 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<Item>, ISpawnable, IObjectPropert
|
|||
{
|
||||
VerifyCompactInfo();
|
||||
}
|
||||
|
||||
if (oldValue != value)
|
||||
{
|
||||
UpdateDecayRegistration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2268,9 +2279,22 @@ public partial class Item : IHued, IComparable<Item>, 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<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
|
||||
item.Delta(ItemDelta.Update);
|
||||
item.UpdateDecayRegistration();
|
||||
|
||||
item.OnAdded(this);
|
||||
OnItemAdded(item);
|
||||
|
|
@ -3374,6 +3399,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
|
||||
item.Parent = null;
|
||||
item.UpdateDecayRegistration();
|
||||
|
||||
item.OnRemoved(this);
|
||||
OnItemRemoved(item);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2458,6 +2458,8 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
m_PropertyList = null;
|
||||
}
|
||||
|
||||
public virtual bool SkipSerialization => false;
|
||||
|
||||
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
||||
public Map Map
|
||||
{
|
||||
|
|
|
|||
|
|
@ -98,10 +98,19 @@ public class GenericEntityPersistence<T> : 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<T> : 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()
|
||||
|
|
|
|||
|
|
@ -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<Item> _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<Mobile>
|
||||
|
|
|
|||
|
|
@ -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<UOContentFixture>, IDisposabl
|
|||
World.Load();
|
||||
|
||||
World.ExitSerializationThreads();
|
||||
|
||||
DecayScheduler.Configure();
|
||||
}
|
||||
|
||||
private static int _counter;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue