From f2de2fbb77425b6b4488d13886829af19feef40f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 1 Oct 2023 22:10:47 -0700 Subject: [PATCH] fix: Cleans up entity persistence. Generalizes Mobiles/Items/Guilds (#1528) ### Summary - [X] Fixed a bug where entity persistence was serialized out of order, causing world corruption. - [X] Fixed LastSerialized not being utilized properly and dangling references still becoming an issue. - [X] Added a new `GenericEntityPersistence` type to encapsulate `ISerializable` serialization. - [X] Removing the custom logic and moved Items, Mobiles, Guilds, and Accounts to GenericEntityPersistence. - [X] Changed serialization to use the singleton pattern to reduce calling methods from stored variables. --- .../Server/Serialization/AdhocPersistence.cs | 2 +- .../Serialization/GenericEntityPersistence.cs | 311 ++++++++++ .../GenericEntitySerialization.cs | 209 ------- .../Serialization/GenericPersistence.cs | 52 +- Projects/Server/Serialization/Persistence.cs | 82 ++- Projects/Server/World/EntityPersistence.cs | 9 +- Projects/Server/World/World.cs | 579 ++++-------------- Projects/UOContent/Accounting/Accounts.cs | 194 +++--- .../Engines/Bulk Orders/Books/BOBEntries.cs | 16 +- .../Engines/Bulk Orders/Books/BaseBOBEntry.cs | 6 +- .../Bulk Orders/Books/BulkOrderBook.cs | 4 +- .../Engines/CannedEvil/ChampionTitleSystem.cs | 24 +- .../Engines/Factions/Core/FactionSystem.cs | 17 +- .../PlayerMurderSystem.cs | 26 +- .../Engines/Stealables/StealableArtifacts.cs | 15 +- .../UOContent/Engines/Virtues/Compassion.cs | 2 +- Projects/UOContent/Engines/Virtues/Honor.cs | 6 +- .../UOContent/Engines/Virtues/HonorContext.cs | 2 +- Projects/UOContent/Engines/Virtues/Justice.cs | 16 +- .../UOContent/Engines/Virtues/Sacrifice.cs | 6 +- Projects/UOContent/Engines/Virtues/Valor.cs | 4 +- .../UOContent/Engines/Virtues/VirtueGump.cs | 2 +- .../Engines/Virtues/VirtueInfoGump.cs | 2 +- .../Engines/Virtues/VirtueStatusGump.cs | 2 +- .../UOContent/Engines/Virtues/VirtueSystem.cs | 42 +- .../Skill Items/Thief/DisguisePersistence.cs | 13 +- .../UOContent/Items/Weapons/BaseWeapon.cs | 2 +- Projects/UOContent/Misc/StaminaSystem.cs | 12 +- Projects/UOContent/Misc/Titles.cs | 2 +- Projects/UOContent/Mobiles/AI/BaseAI.cs | 2 +- Projects/UOContent/Mobiles/BaseCreature.cs | 2 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 24 +- .../Mobiles/Townfolk/BaseEscortable.cs | 2 +- Projects/UOContent/Skills/AnimalTaming.cs | 2 +- 34 files changed, 721 insertions(+), 970 deletions(-) create mode 100644 Projects/Server/Serialization/GenericEntityPersistence.cs delete mode 100644 Projects/Server/Serialization/GenericEntitySerialization.cs diff --git a/Projects/Server/Serialization/AdhocPersistence.cs b/Projects/Server/Serialization/AdhocPersistence.cs index effb6cd24..0e1e9ebb5 100644 --- a/Projects/Server/Serialization/AdhocPersistence.cs +++ b/Projects/Server/Serialization/AdhocPersistence.cs @@ -51,7 +51,7 @@ public static class AdhocPersistence /** * Serializes to a memory buffer synchronously, then flushes to the path asynchronously. - * See WriteSnapshot for more info about how the snapshot. + * See WriteSnapshot for more info about how to snapshot. */ public static void SerializeAndSnapshot(string filePath, Action serializer, ConcurrentQueue types = null) { diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs new file mode 100644 index 000000000..ce8a7add1 --- /dev/null +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -0,0 +1,311 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2023 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: GenericEntityPersistence.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; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Server.Logging; + +namespace Server; + +public interface IGenericEntityPersistence +{ + public void DeserializeIndexes(string savePath, Dictionary typesDb); +} + +public class GenericEntityPersistence : Persistence, IGenericEntityPersistence where T : class, ISerializable +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntityPersistence)); + + private static List> _entities; + + private string _name; + private Serial _lastEntitySerial; + private readonly Dictionary _pendingAdd = new(); + private readonly Dictionary _pendingDelete = new(); + private uint _minSerial; + private uint _maxSerial; + + public Dictionary EntitiesBySerial { get; private set; } = new(); + + public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : base(priority) + { + _name = name; + _minSerial = minSerial; + _maxSerial = maxSerial; + typeof(T).RegisterFindEntity(Find); + } + + public override void Serialize() + { + // TODO: Hand off to a scheduler instead + Parallel.ForEach(EntitiesBySerial.Values, SerializeEntity); + } + + protected virtual void SerializeEntity(T entity) + { + entity.Serialize(World.SerializedTypes); + } + + public override void WriteSnapshot(string basePath) + { + IIndexInfo indexInfo = new EntityTypeIndex(_name); + EntityPersistence.WriteEntities(indexInfo, EntitiesBySerial, basePath,World.SerializedTypes, out _); + } + + public virtual void DeserializeIndexes(string savePath, Dictionary typesDb) + { + IIndexInfo indexInfo = new EntityTypeIndex(_name); + + EntitiesBySerial = EntityPersistence.LoadIndex(savePath, indexInfo, typesDb, out _entities); + + if (EntitiesBySerial.Count > 0) + { + _lastEntitySerial = EntitiesBySerial.Keys.Max(); + } + } + + public override void Deserialize(string savePath, Dictionary typesDb) + { + IIndexInfo indexInfo = new EntityTypeIndex(_name); + EntityPersistence.LoadData(savePath, indexInfo, typesDb, _entities); + _entities = null; + } + + public override void PostSerialize() + { + ProcessSafetyQueues(); + } + + public override void PostDeserialize() + { + ProcessSafetyQueues(); + } + + public Serial NewEntity + { + get + { +#if THREADGUARD + if (Thread.CurrentThread != Core.Thread) + { + logger.Error( + "Attempted to get a new entity serial from the wrong thread!\n{StackTrace}", + new StackTrace() + ); + } +#endif + var last = _lastEntitySerial; + var max = (Serial)_maxSerial; + + for (uint i = 0; i < _maxSerial; i++) + { + last++; + + if (last > max) + { + last = (Serial)_minSerial; + } + + if (FindEntity(last) == null) + { + return _lastEntitySerial = last; + } + } + + OutOfMemory($"No serials left to allocate for {_name}"); + return Serial.MinusOne; + } + } + + public void AddEntity(T entity) + { + var worldState = World.WorldState; + switch (worldState) + { + default: // Not Running + { + throw new Exception($"Added {entity.GetType().Name} before world load."); + } + case WorldState.Saving: + { + AppendSafetyLog("add", entity); + goto case WorldState.WritingSave; + } + case WorldState.Loading: + case WorldState.WritingSave: + { + if (_pendingDelete.Remove(entity.Serial)) + { + logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString()); + } + + _pendingAdd[entity.Serial] = entity; + break; + } + case WorldState.Running: + { + ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(EntitiesBySerial, entity.Serial, out bool exists); + if (exists) + { + if (entityEntry == entity) + { + logger.Error( + $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", + entity.GetType().FullName, + entity.Serial, + new StackTrace() + ); + } + else + { + logger.Error( + $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", + entity.GetType().FullName, + entity.Serial, + entityEntry.GetType().FullName, + entityEntry.Serial, + new StackTrace() + ); + } + } + else + { + entityEntry = entity; + } + break; + } + } + } + + public void RemoveEntity(T entity) + { + var worldState = World.WorldState; + switch (worldState) + { + default: // Not Running + { + throw new Exception($"Removed {entity.GetType().Name} before world load."); + } + case WorldState.Saving: + { + AppendSafetyLog("delete", entity); + goto case WorldState.WritingSave; + } + case WorldState.Loading: + case WorldState.WritingSave: + { + _pendingAdd.Remove(entity.Serial); + _pendingDelete[entity.Serial] = entity; + break; + } + case WorldState.Running: + { + EntitiesBySerial.Remove(entity.Serial); + break; + } + } + } + + private void ProcessSafetyQueues() + { + foreach (var entity in _pendingAdd.Values) + { + AddEntity(entity); + } + + _pendingAdd.Clear(); + + foreach (var entity in _pendingDelete.Values) + { + if (_pendingAdd.ContainsKey(entity.Serial)) + { + logger.Warning("Entity {Entity} was both pending deletion and addition after save", entity); + } + + RemoveEntity(entity); + } + + _pendingDelete.Clear(); + } + + private void AppendSafetyLog(string action, ISerializable entity) + { + var message = + $"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected."; + + logger.Information(message, action, entity); + + try + { + using var op = new StreamWriter("world-save-errors.log", true); + op.WriteLine("{0}\t{1}", DateTime.UtcNow, message); + op.WriteLine(new StackTrace(2).ToString()); + op.WriteLine(); + } + catch + { + // ignored + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Find(Serial serial) => FindEntity(serial, false, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Find(Serial serial, bool returnDeleted) => FindEntity(serial, returnDeleted, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity(serial, returnDeleted, returnPending); + + public R FindEntity(Serial serial) where R : class, T => FindEntity(serial, false, false); + + public R FindEntity(Serial serial, bool returnDeleted, bool returnPending) where R : class, T + { + switch (World.WorldState) + { + default: return null; + case WorldState.Loading: + case WorldState.Saving: + case WorldState.WritingSave: + { + if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) + { + return entity as R; + } + + if (returnPending && _pendingAdd.TryGetValue(serial, out entity) || + EntitiesBySerial.TryGetValue(serial, out entity)) + { + return entity as R; + } + + return null; + } + case WorldState.Running: + { + return EntitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); +} diff --git a/Projects/Server/Serialization/GenericEntitySerialization.cs b/Projects/Server/Serialization/GenericEntitySerialization.cs deleted file mode 100644 index 6e34a0d9d..000000000 --- a/Projects/Server/Serialization/GenericEntitySerialization.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Server.Logging; - -namespace Server; - -public class GenericEntitySerialization where T : class, ISerializable -{ - private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntitySerialization)); - - private static string _systemName; - private static Serial _lastEntitySerial; - private static readonly Dictionary _pendingAdd = new(); - private static readonly Dictionary _pendingDelete = new(); - private static Dictionary _entitiesBySerial = new(); - - public static void Configure(string systemName) - { - _systemName = systemName; - typeof(T).RegisterFindEntity(Find); - Persistence.Register(_systemName, Serialize, WriteSnapshot, Deserialize); - } - - internal static void Serialize() - { - EntityPersistence.SaveEntities( - _entitiesBySerial.Values, - entity => entity.Serialize(World.SerializedTypes) - ); - } - - internal static void WriteSnapshot(string basePath) - { - IIndexInfo indexInfo = new EntityTypeIndex(_systemName); - EntityPersistence.WriteEntities(indexInfo, _entitiesBySerial, basePath,World.SerializedTypes, out _); - } - - internal static void Deserialize(string path, Dictionary typesDb) - { - IIndexInfo indexInfo = new EntityTypeIndex(_systemName); - - _entitiesBySerial = EntityPersistence.LoadIndex(path, indexInfo, typesDb, out List> entities); - - if (_entitiesBySerial.Count > 0) - { - _lastEntitySerial = _entitiesBySerial.Keys.Max(); - } - - EntityPersistence.LoadData(path, indexInfo, typesDb, entities); - } - - public static Serial NewEntity - { - get - { -#if THREADGUARD - if (Thread.CurrentThread != Core.Thread) - { - logger.Error( - "Attempted to get a new entity serial from the wrong thread!\n{StackTrace}", - new StackTrace() - ); - } -#endif - var last = _lastEntitySerial; - - for (uint i = 0; i < uint.MaxValue; i++) - { - last++; - - if (FindEntity(last) == null) - { - return _lastEntitySerial = last; - } - } - - OutOfMemory("No serials left to allocate for BOBEntries"); - return Serial.MinusOne; - } - } - - public static void AddEntity(T entity) - { - var worldState = World.WorldState; - switch (worldState) - { - default: // Not Running - { - throw new Exception($"Added {entity.GetType().Name} before world load."); - } - case WorldState.Saving: - case WorldState.Loading: - case WorldState.WritingSave: - { - if (_pendingDelete.Remove(entity.Serial)) - { - logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString()); - } - - _pendingAdd[entity.Serial] = entity; - break; - } - case WorldState.Running: - { - ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(_entitiesBySerial, entity.Serial, out bool exists); - if (exists) - { - if (entityEntry == entity) - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - new StackTrace() - ); - } - else - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - entityEntry.GetType().FullName, - entityEntry.Serial, - new StackTrace() - ); - } - } - else - { - entityEntry = entity; - } - break; - } - } - } - - public static void RemoveEntity(T entity) - { - var worldState = World.WorldState; - switch (worldState) - { - default: // Not Running - { - throw new Exception($"Removed {entity.GetType().Name} before world load."); - } - case WorldState.Saving: - case WorldState.Loading: - case WorldState.WritingSave: - { - _pendingAdd.Remove(entity.Serial); - _pendingDelete[entity.Serial] = entity; - break; - } - case WorldState.Running: - { - _entitiesBySerial.Remove(entity.Serial); - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T Find(Serial serial) => FindEntity(serial, false, false); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T Find(Serial serial, bool returnDeleted) => FindEntity(serial, returnDeleted, false); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity(serial, returnDeleted, returnPending); - - public static R FindEntity(Serial serial) where R : class, T => FindEntity(serial, false, false); - - public static R FindEntity(Serial serial, bool returnDeleted, bool returnPending) where R : class, T - { - switch (World.WorldState) - { - default: return null; - case WorldState.Loading: - case WorldState.Saving: - case WorldState.WritingSave: - { - if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) - { - return entity as R; - } - - if (returnPending && _pendingAdd.TryGetValue(serial, out entity) || - _entitiesBySerial.TryGetValue(serial, out entity)) - { - return entity as R; - } - - return null; - } - case WorldState.Running: - { - return _entitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); -} diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index 85d563fc4..2b9ab5941 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -19,35 +19,33 @@ using System.IO; namespace Server; -public static class GenericPersistence +public abstract class GenericPersistence : Persistence { - public static void Register( - string name, - Action serializer, - Action deserializer, - int priority = Persistence.DefaultPriority - ) + private BufferWriter _saveBuffer; + + public string Name { get; } + + public GenericPersistence(string name, int priority) : base(priority) => Name = name; + + public override void Serialize() { - BufferWriter saveBuffer = null; + _saveBuffer ??= new BufferWriter(true, World.SerializedTypes); + _saveBuffer.Seek(0, SeekOrigin.Begin); - void Serialize() - { - saveBuffer ??= new BufferWriter(true, World.SerializedTypes); - saveBuffer.Seek(0, SeekOrigin.Begin); - - serializer(saveBuffer); - } - - void WriteSnapshot(string savePath) - { - string binPath = Path.Combine(savePath, name, $"{name}.bin"); - var buffer = saveBuffer!.Buffer.AsSpan(0, (int)saveBuffer.Position); - AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer); - } - - void Deserialize(string savePath, Dictionary typesDb) => - AdhocPersistence.Deserialize(Path.Combine(savePath, name, $"{name}.bin"), deserializer); - - Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, priority); + Serialize(_saveBuffer); } + + public abstract void Serialize(IGenericWriter writer); + + public override void WriteSnapshot(string basePath) + { + string binPath = Path.Combine(basePath, Name, $"{Name}.bin"); + var buffer = _saveBuffer!.Buffer.AsSpan(0, (int)_saveBuffer.Position); + AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer); + } + + public override void Deserialize(string savePath, Dictionary typesDb) => + AdhocPersistence.Deserialize(Path.Combine(savePath, Name, $"{Name}.bin"), Deserialize); + + public abstract void Deserialize(IGenericReader reader); } diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index 702bb918e..37c240b9d 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -21,38 +21,31 @@ using System.Threading.Tasks; namespace Server; -public static class Persistence +public abstract class Persistence { - public const int DefaultPriority = 100; + private static readonly SortedSet _registry = new(new PersistenceComparer()); - private static readonly SortedSet _registry = new(new RegistryEntryComparer()); + public int Priority { get; } - public static void Register( - string name, - Action serializer, - Action snapshotWriter, - Action> deserializer, - int priority = DefaultPriority - ) + public Persistence(int priority = 100) { - _registry.Add( - new RegistryEntry - { - Name = name, - Priority = priority, - Serialize = serializer, - WriteSnapshot = snapshotWriter, - Deserialize = deserializer - } - ); + Priority = priority; + _registry.Add(this); } - public static void Unregister(string name) => _registry.RemoveWhere(entry => entry.Name == name); + public bool Register() => _registry.Add(this); + + public void Unregister() => _registry.Remove(this); public static void Load(string path) { var typesDb = LoadTypes(path); + foreach (var entry in _registry) + { + (entry as IGenericEntityPersistence)?.DeserializeIndexes(path, typesDb); + } + // This should probably not be parallel since Mobiles must be loaded before Items foreach (var entry in _registry) { @@ -86,9 +79,26 @@ public static class Persistence return db; } - public static void Serialize() + internal static void SerializeAll() { - Parallel.ForEach(_registry, entry => entry.Serialize()); + // TODO: Hand off to a scheduler + Parallel.ForEach(_registry, p => p.Serialize()); + } + + internal static void PostSerializeAll() + { + foreach (var p in _registry) + { + p.PostSerialize(); + } + } + + internal static void PostDeserializeAll() + { + foreach (var p in _registry) + { + p.PostDeserialize(); + } } public static void WriteSnapshot(string path, ConcurrentQueue types) @@ -126,20 +136,24 @@ public static class Persistence } } - public record RegistryEntry - { - public string Name { get; init; } - public int Priority { get; init; } + // Serializes to memory buffers and run in parallel + public abstract void Serialize(); - // Serializes to memory buffers and run in parallel - public Action Serialize { get; init; } - public Action WriteSnapshot { get; init; } - public Action> Deserialize { get; init; } + public abstract void WriteSnapshot(string savePath); + + public abstract void Deserialize(string savePath, Dictionary typesDb); + + public virtual void PostSerialize() + { } - internal class RegistryEntryComparer : IComparer + public virtual void PostDeserialize() { - public int Compare(RegistryEntry x, RegistryEntry y) + } + + internal class PersistenceComparer : IComparer + { + public int Compare(Persistence x, Persistence y) { if (x == y) { @@ -160,7 +174,7 @@ public static class Persistence var cmp = x.Priority.CompareTo(y.Priority); // Then alphabetically. We won't allow the same entry (by name) twice in the SortedSet - return cmp != 0 ? cmp : x.Name?.CompareOrdinal(y.Name) ?? -1; + return cmp != 0 ? cmp : x.GetHashCode().CompareTo(y.GetHashCode()); } } diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs index e11205c94..18174fe70 100644 --- a/Projects/Server/World/EntityPersistence.cs +++ b/Projects/Server/World/EntityPersistence.cs @@ -19,8 +19,6 @@ using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; namespace Server; @@ -72,12 +70,6 @@ public static class EntityPersistence } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SaveEntities( - IEnumerable list, - Action serializer - ) where T : class, ISerializable => Parallel.ForEach(list, serializer); - public static Dictionary LoadIndex( string path, IIndexInfo indexInfo, @@ -176,6 +168,7 @@ public static class EntityPersistence } idxReader.Close(); + entities.TrimExcess(); return map; } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index ba51f033b..7d3be696a 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -17,10 +17,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading; using Server.Guilds; using Server.Logging; @@ -41,120 +38,34 @@ public static class World { private static ILogger logger = LogFactory.GetLogger(typeof(World)); + private static ItemPersistence _itemPersistence = new(); + private static MobilePersistence _mobilePersistence = new(); + private static GenericEntityPersistence _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF); + private static ManualResetEvent m_DiskWriteHandle = new(true); - private static Dictionary _pendingAdd = new(); - private static Dictionary _pendingDelete = new(); private static ConcurrentQueue _decayQueue = new(); private static string _tempSavePath; // Path to the temporary folder for the save - private static bool _enableSaveStats; public const bool DirtyTrackingEnabled = false; public const uint ItemOffset = 0x40000000; public const uint MaxItemSerial = 0x7FFFFFFF; public const uint MaxMobileSerial = ItemOffset - 1; - private const uint _maxItems = MaxItemSerial - ItemOffset + 1; - private static Serial _lastMobile = Serial.Zero; - private static Serial _lastItem = (Serial)ItemOffset; - private static Serial _lastGuild = Serial.Zero; + public static Serial NewMobile => _mobilePersistence.NewEntity; + public static Serial NewItem => _itemPersistence.NewEntity; + public static Serial NewGuild => _guildPersistence.NewEntity; - public static Serial NewMobile - { - get - { -#if THREADGUARD - if (Thread.CurrentThread != Core.Thread) - { - logger.Error( - "Attempted to get a new mobile serial from the wrong thread!\n{StackTrace}", - new StackTrace() - ); - } -#endif - var last = _lastMobile; - var maxMobile = (Serial)MaxMobileSerial; - - for (int i = 0; i < MaxMobileSerial; i++) - { - last++; - - if (last > maxMobile) - { - last = (Serial)1; - } - - if (FindMobile(last, true) == null) - { - return _lastMobile = last; - } - } - - OutOfMemory("No serials left to allocate for mobiles"); - return Serial.MinusOne; - } - } - - public static Serial NewItem - { - get - { -#if THREADGUARD - if (Thread.CurrentThread != Core.Thread) - { - logger.Error( - "Attempted to get a new item serial from the wrong thread!\n{StackTrace}", - new StackTrace() - ); - } -#endif - var last = _lastItem; - - for (int i = 0; i < _maxItems; i++) - { - last++; - - if (last > MaxItemSerial) - { - last = (Serial)ItemOffset; - } - - if (FindItem(last, true) == null) - { - return _lastItem = last; - } - } - - OutOfMemory("No serials left to allocate for items"); - return Serial.MinusOne; - } - } - - public static Serial NewGuild - { - get - { - while (FindGuild(_lastGuild += 1) != null) - { - } - - return _lastGuild; - } - } - - private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); + public static Dictionary Items => _itemPersistence.EntitiesBySerial; + public static Dictionary Mobiles => _mobilePersistence.EntitiesBySerial; + public static Dictionary Guilds => _guildPersistence.EntitiesBySerial; public static string SavePath { get; private set; } - public static WorldState WorldState { get; private set; } public static bool Saving => WorldState == WorldState.Saving; public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial; public static bool Loading => WorldState == WorldState.Loading; - public static Dictionary Mobiles { get; private set; } - public static Dictionary Items { get; private set; } - public static Dictionary Guilds { get; private set; } - public static void Configure() { var tempSavePath = ServerConfiguration.GetSetting("world.tempSavePath", "temp"); @@ -162,11 +73,6 @@ public static class World var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves"); SavePath = PathUtility.GetFullPath(savePath); - - _enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false); - - // Mobiles & Items - Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -243,36 +149,6 @@ public static class World NetState.FlushAll(); } - internal static void LoadEntities(string basePath, Dictionary typesDb) - { - IIndexInfo itemIndexInfo = new EntityTypeIndex("Items"); - IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); - IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - - Mobiles = EntityPersistence.LoadIndex(basePath, mobileIndexInfo, typesDb, out List> mobiles); - Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, typesDb, out List> items); - Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, typesDb, out List> guilds); - - if (Mobiles.Count > 0) - { - _lastMobile = Mobiles.Keys.Max(); - } - - if (Items.Count > 0) - { - _lastItem = Items.Keys.Max(); - } - - if (Guilds.Count > 0) - { - _lastGuild = Guilds.Keys.Max(); - } - - EntityPersistence.LoadData(basePath, mobileIndexInfo, typesDb, mobiles); - EntityPersistence.LoadData(basePath, itemIndexInfo, typesDb, items); - EntityPersistence.LoadData(basePath, guildIndexInfo, typesDb, guilds); - } - public static void Load() { if (WorldState != WorldState.Initial) @@ -291,25 +167,7 @@ public static class World // Set the world to running before we process our queues WorldState = WorldState.Running; - ProcessSafetyQueues(); - - foreach (var item in Items.Values) - { - if (item.Parent == null) - { - item.UpdateTotals(); - } - - item.ClearProperties(); - } - - foreach (var m in Mobiles.Values) - { - m.UpdateRegion(); // Is this really needed? - m.UpdateTotals(); - - m.ClearProperties(); - } + Persistence.PostDeserializeAll(); // Process safety queues watch.Stop(); @@ -321,102 +179,13 @@ public static class World ); } - private static void ProcessSafetyQueues() - { - foreach (var entity in _pendingAdd.Values) - { - AddEntity(entity); - } - - _pendingAdd.Clear(); - - foreach (var entity in _pendingDelete.Values) - { - if (_pendingAdd.ContainsKey(entity.Serial)) - { - logger.Warning("Entity {Entity} was both pending deletion and addition after save", entity); - } - - RemoveEntity(entity); - } - - _pendingDelete.Clear(); - } - - private static void AppendSafetyLog(string action, ISerializable entity) - { - var message = - $"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected."; - - logger.Information(message, action, entity); - - try - { - using var op = new StreamWriter("world-save-errors.log", true); - op.WriteLine("{0}\t{1}", DateTime.UtcNow, message); - op.WriteLine(new StackTrace(2).ToString()); - op.WriteLine(); - } - catch - { - // ignored - } - } - private static void FinishWorldSave() { WorldState = WorldState.Running; ProcessDecay(); - ProcessSafetyQueues(); - } - private static void TraceSave(params IEnumerable>[] entityTypes) - { - try - { - int count = 0; - - var timestamp = Utility.GetTimeStamp(); - var saveStatsPath = Path.Combine(Core.BaseDirectory, $"Logs/Saves/Save-Stats-{timestamp}.log"); - PathUtility.EnsureDirectory(saveStatsPath); - - using var op = new StreamWriter(saveStatsPath, true); - - for (var i = 0; i < entityTypes.Length; i++) - { - foreach (var (t, c) in entityTypes[i]) - { - op.WriteLine("{0}: {1}", t, c); - count++; - } - } - - op.WriteLine("- Total: {0}", count); - - op.WriteLine(); - op.WriteLine(); - } - catch - { - // ignored - } - } - - internal static void WriteEntities(string basePath) - { - IIndexInfo itemIndexInfo = new EntityTypeIndex("Items"); - IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); - IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - - EntityPersistence.WriteEntities(mobileIndexInfo, Mobiles, basePath, SerializedTypes, out var mobileCounts); - EntityPersistence.WriteEntities(itemIndexInfo, Items, basePath, SerializedTypes, out var itemCounts); - EntityPersistence.WriteEntities(guildIndexInfo, Guilds, basePath, SerializedTypes, out var guildCounts); - - if (_enableSaveStats) - { - TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList()); - } + Persistence.PostSerializeAll(); // Process safety queues } public static void WriteFiles(object state) @@ -516,25 +285,6 @@ public static class World */ public static ConcurrentQueue SerializedTypes { get; } = new(); - private static void SaveEntities() - { - _serializationStart = DateTime.UtcNow; - EntityPersistence.SaveEntities(Items.Values, SaveEntity); - EntityPersistence.SaveEntities(Mobiles.Values, SaveEntity); - EntityPersistence.SaveEntities(Guilds.Values, SaveEntity); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void SaveEntity(T entity) where T : class, ISerializable - { - if (entity is Item item && item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart) - { - EnqueueForDecay(item); - } - - entity.Serialize(SerializedTypes); - } - public static void Save() { if (WorldState != WorldState.Running) @@ -558,7 +308,8 @@ public static class World try { - Persistence.Serialize(); + _serializationStart = Core.Now; + Persistence.SerializeAll(); EventSink.InvokeWorldSave(); } catch (Exception ex) @@ -593,231 +344,119 @@ public static class World } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = true) => - FindEntity(serial, returnDeleted, returnPending); - - public static T FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = true) where T : class, IEntity - { - switch (WorldState) - { - default: return default; - case WorldState.Loading: - case WorldState.Saving: - case WorldState.WritingSave: - { - if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) - { - return entity as T; - } - - if (!returnPending || !_pendingAdd.TryGetValue(serial, out entity)) - { - if (serial.IsItem) - { - if (Items.TryGetValue(serial, out var item)) - { - return item as T; - } - } - else // if (serial.IsMobile) - { - if (Mobiles.TryGetValue(serial, out var mob)) - { - return mob as T; - } - } - } - - return null; - } - case WorldState.Running: - { - if (serial.IsItem) - { - return Items.TryGetValue(serial, out var item) ? item as T : null; - } - - if (serial.IsMobile) - { - return Mobiles.TryGetValue(serial, out var mob) ? mob as T : null; - } - - return default; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Item FindItem(Serial serial, bool returnDeleted = false) => FindEntity(serial, returnDeleted); + public static Item FindItem(Serial serial, bool returnDeleted = false) => _itemPersistence.Find(serial, returnDeleted); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Mobile FindMobile(Serial serial, bool returnDeleted = false) => - FindEntity(serial, returnDeleted); + _mobilePersistence.Find(serial, returnDeleted); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BaseGuild FindGuild(Serial serial) => Guilds.TryGetValue(serial, out var guild) ? guild : null; - - public static void AddEntity(T entity) where T : class, IEntity + // Legacy: Only used for retrieving Items and Mobiles. + public static void AddEntity(IEntity entity) { - switch (WorldState) + if (entity is Item item) { - default: // Not Running - { - throw new Exception($"Added {entity.GetType().Name} before world load."); - } - case WorldState.Saving: - { - AppendSafetyLog("add", entity); - goto case WorldState.WritingSave; - } - case WorldState.Loading: - case WorldState.WritingSave: - { - if (_pendingDelete.Remove(entity.Serial)) - { - logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, WorldState.ToString()); - } - _pendingAdd[entity.Serial] = entity; - break; - } - case WorldState.Running: - { - if (entity.Serial.IsItem) - { - ref var item = ref CollectionsMarshal.GetValueRefOrAddDefault(Items, entity.Serial, out bool exists); - if (exists) - { - if (item == entity) - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - new StackTrace() - ); - } - else - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - item.GetType().FullName, - item.Serial, - new StackTrace() - ); - } - } - else - { - item = entity as Item; - } - } - - if (entity.Serial.IsMobile) - { - ref var mob = ref CollectionsMarshal.GetValueRefOrAddDefault(Mobiles, entity.Serial, out bool exists); - if (exists) - { - if (mob == entity) - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Mobiles but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - new StackTrace() - ); - } - else - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Mobiles but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - mob.GetType().FullName, - mob.Serial, - new StackTrace() - ); - } - } - else - { - mob = entity as Mobile; - } - } - break; - } + _itemPersistence.AddEntity(item); } - } - - public static void AddGuild(BaseGuild entity) - { - ref var guild = ref CollectionsMarshal.GetValueRefOrAddDefault(Guilds, entity.Serial, out bool exists); - if (exists) + else if (entity is Mobile mobile) { - if (guild == entity) - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Guilds but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - new StackTrace() - ); - } - else - { - logger.Error( - $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Guilds but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", - entity.GetType().FullName, - entity.Serial, - guild.GetType().FullName, - guild.Serial, - new StackTrace() - ); - } + _mobilePersistence.AddEntity(mobile); } else { - guild = entity; + logger.Warning($"Attempted to call World.AddEntity with '{entity.GetType()}'. Must be a mobile or item."); } } - public static void RemoveEntity(T entity) where T : class, IEntity + public static void RemoveEntity(IEntity entity) { - switch (WorldState) + if (entity is Item item) { - default: // Not Running - { - throw new Exception($"Removed {entity.GetType().Name} before world load."); - } - case WorldState.Saving: - { - AppendSafetyLog("delete", entity); - goto case WorldState.WritingSave; - } - case WorldState.Loading: - case WorldState.WritingSave: - { - _pendingAdd.Remove(entity.Serial); - _pendingDelete[entity.Serial] = entity; - break; - } - case WorldState.Running: - { - if (entity.Serial.IsItem) - { - Items.Remove(entity.Serial); - } - - if (entity.Serial.IsMobile) - { - Mobiles.Remove(entity.Serial); - } - break; - } + _itemPersistence.RemoveEntity(item); + } + else if (entity is Mobile mobile) + { + _mobilePersistence.RemoveEntity(mobile); + } + else + { + logger.Warning($"Attempted to call World.RemoveEntity with '{entity.GetType()}'. Must be a mobile or item."); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RemoveGuild(BaseGuild guild) => Guilds.Remove(guild.Serial); + public static BaseGuild FindGuild(Serial serial) => _guildPersistence.Find(serial); + + public static void AddGuild(BaseGuild guild) => _guildPersistence.AddEntity(guild); + + public static void RemoveGuild(BaseGuild guild) => _guildPersistence.RemoveEntity(guild); + + // Legacy: Only used for retrieving Items and Mobiles. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = false) => + FindEntity(serial, returnDeleted, returnPending); + + // Legacy: Only used for retrieving Items and Mobiles. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = false) + where T : class, IEntity + { + if (serial.IsItem) + { + return _itemPersistence.Find(serial, returnDeleted, returnPending) as T; + } + + return _mobilePersistence.Find(serial, returnDeleted, returnPending) as T; + } + + private class ItemPersistence : GenericEntityPersistence + { + public ItemPersistence() : base("Items", 2, ItemOffset, MaxItemSerial) + { + } + + protected override void SerializeEntity(Item item) + { + if (item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart) + { + EnqueueForDecay(item); + } + + ((ISerializable)item).Serialize(SerializedTypes); + } + + public override void PostDeserialize() + { + base.PostDeserialize(); + + foreach (var item in EntitiesBySerial.Values) + { + if (item.Parent == null) + { + item.UpdateTotals(); + } + + item.ClearProperties(); + } + } + } + + private class MobilePersistence : GenericEntityPersistence + { + public MobilePersistence() : base("Mobiles", 1, 1, MaxMobileSerial) + { + } + + public override void PostDeserialize() + { + base.PostDeserialize(); + + foreach (var m in EntitiesBySerial.Values) + { + m.UpdateRegion(); // Is this really needed? + m.UpdateTotals(); + + m.ClearProperties(); + } + } + } } diff --git a/Projects/UOContent/Accounting/Accounts.cs b/Projects/UOContent/Accounting/Accounts.cs index b71dfad7e..7be2410a8 100644 --- a/Projects/UOContent/Accounting/Accounts.cs +++ b/Projects/UOContent/Accounting/Accounts.cs @@ -1,139 +1,95 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Xml; using Server.Logging; -namespace Server.Accounting +namespace Server.Accounting; + +public class Accounts : GenericEntityPersistence { - public static class Accounts + private static readonly ILogger logger = LogFactory.GetLogger(typeof(Accounts)); + + private static readonly Dictionary _accountsByName = new(32, StringComparer.OrdinalIgnoreCase); + + public static int Count => _accountsByName.Count; + + private static Accounts _accountsPersistence; + + public static Serial NewAccount => _accountsPersistence.NewEntity; + + public static void Configure() { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(Accounts)); + _accountsPersistence = new Accounts(); + } - private static readonly Dictionary _accountsByName = new(32, StringComparer.OrdinalIgnoreCase); - private static Dictionary _accountsById = new(32); - private static Serial _lastAccount; + public Accounts() : base("Accounts", 3, 0x1, 0x7FFFFFFF) + { + } - private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); + public static IEnumerable GetAccounts() => _accountsByName.Values; - public static Serial NewAccount + public static Account GetAccount(string username) + { + _accountsByName.TryGetValue(username, out var a); + return a; + } + + public static void Add(Account a) + { + _accountsByName[a.Username] = a; + _accountsPersistence.AddEntity(a); + } + + public static void Remove(Account a) + { + _accountsByName.Remove(a.Username); + _accountsPersistence.RemoveEntity(a); + } + + public override void Deserialize(string path, Dictionary typesDb) + { + var filePath = Path.Combine(path, "Accounts", "accounts.xml"); + + // Backward Compatibility + if (File.Exists(filePath)) { - get - { - var last = _lastAccount; - - for (uint i = 0; i < uint.MaxValue; i++) - { - last++; - - if (FindAccount(last) == null) - { - return _lastAccount = last; - } - } - - OutOfMemory("No serials left to allocate for accounts"); - return Serial.MinusOne; - } + DeserializeXml(filePath); + return; } - public static int Count => _accountsByName.Count; + base.Deserialize(path, typesDb); - public static void Configure() => - Persistence.Register("Accounts", Serialize, WriteSnapshot, Deserialize); - - internal static void Serialize() - { - EntityPersistence.SaveEntities( - _accountsById.Values, - account => ((ISerializable)account).Serialize(World.SerializedTypes) - ); - } - - internal static void WriteSnapshot(string basePath) - { - IIndexInfo indexInfo = new EntityTypeIndex("Accounts"); - EntityPersistence.WriteEntities(indexInfo, _accountsById, basePath,World.SerializedTypes, out _); - } - - public static IEnumerable GetAccounts() => _accountsByName.Values; - - public static Account GetAccount(string username) - { - _accountsByName.TryGetValue(username, out var a); - return a; - } - - public static void Add(Account a) + foreach (var a in EntitiesBySerial.Values) { _accountsByName[a.Username] = a; - _accountsById[a.Serial] = a; - } - - public static void Remove(Account a) - { - _accountsByName.Remove(a.Username); - _accountsById.Remove(a.Serial); - } - - internal static void Deserialize(string path, Dictionary typesDb) - { - var filePath = Path.Combine(path, "Accounts", "accounts.xml"); - - // Backward Compatibility - if (File.Exists(filePath)) - { - DeserializeXml(filePath); - return; - } - - IIndexInfo indexInfo = new EntityTypeIndex("Accounts"); - - _accountsById = EntityPersistence.LoadIndex(path, indexInfo, typesDb, out List> accounts); - - if (_accountsById.Count > 0) - { - _lastAccount = _accountsById.Keys.Max(); - } - - EntityPersistence.LoadData(path, indexInfo, typesDb, accounts); - - foreach (var a in _accountsById.Values) - { - _accountsByName[a.Username] = a; - } - } - - private static void DeserializeXml(string filePath) - { - var doc = new XmlDocument(); - doc.Load(filePath); - - var root = doc["accounts"]; - - if (root == null) - { - throw new FileLoadException("Unable to load xml file"); - } - - foreach (XmlElement account in root.GetElementsByTagName("account")) - { - try - { - new Account(account); - } - catch - { - logger.Warning("Account instance load failed"); - } - } - } - - public static IAccount FindAccount(Serial serial) - { - _accountsById.TryGetValue(serial, out var account); - return account; } } + + private static void DeserializeXml(string filePath) + { + var doc = new XmlDocument(); + doc.Load(filePath); + + var root = doc["accounts"]; + + if (root == null) + { + throw new FileLoadException("Unable to load xml file"); + } + + foreach (XmlElement account in root.GetElementsByTagName("account")) + { + try + { + new Account(account); + } + catch + { + logger.Warning("Account instance load failed"); + } + } + } + + public static IAccount FindAccount(Serial serial) => _accountsPersistence.FindEntity(serial); } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs index f40f03906..ce3c02e8c 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs @@ -1,9 +1,21 @@ namespace Server.Engines.BulkOrders; -public class BOBEntries : GenericEntitySerialization +public class BOBEntries : GenericEntityPersistence { + private static BOBEntries _bobEntriesPersistence; + public static void Configure() { - Configure("BOBEntries"); + _bobEntriesPersistence = new BOBEntries(); } + + public BOBEntries() : base("BOBEntries", 3, 0x1, 0x7FFFFFFF) + { + } + + public static Serial NewBOBEntry => _bobEntriesPersistence.NewEntity; + + public static void Add(IBOBEntry entity) => _bobEntriesPersistence.AddEntity(entity); + + public static void Remove(IBOBEntry entity) => _bobEntriesPersistence.AddEntity(entity); } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs index ace0a0227..c93d3658f 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs @@ -32,14 +32,14 @@ public abstract partial class BaseBOBEntry : IBOBEntry public BaseBOBEntry() { - Serial = BOBEntries.NewEntity; - BOBEntries.AddEntity(this); + Serial = BOBEntries.NewBOBEntry; + BOBEntries.Add(this); } public virtual void Delete() { Deleted = true; - BOBEntries.RemoveEntity(this); + BOBEntries.Remove(this); } public abstract Item Reconstruct(); diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs index 571cc28b8..df7334310 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs @@ -245,7 +245,7 @@ namespace Server.Engines.BulkOrders { case 0: { - var largeEntry = new BOBLargeEntry(BOBEntries.NewEntity); + var largeEntry = new BOBLargeEntry(BOBEntries.NewBOBEntry); largeEntry.Deserialize(reader); Entries.Add(largeEntry); @@ -253,7 +253,7 @@ namespace Server.Engines.BulkOrders } case 1: { - var smallEntry = new BOBSmallEntry(BOBEntries.NewEntity); + var smallEntry = new BOBSmallEntry(BOBEntries.NewBOBEntry); smallEntry.Deserialize(reader); Entries.Add(smallEntry); break; diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs index bbb8f92bc..13e7afd80 100644 --- a/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs @@ -6,8 +6,10 @@ using Server.Mobiles; namespace Server.Engines.CannedEvil; -public static class ChampionTitleSystem +public class ChampionTitleSystem : GenericPersistence { + private static ChampionTitleSystem _championTitlePersistence; + // All of the players with murders private static readonly Dictionary _championTitleContexts = new(); @@ -15,7 +17,7 @@ public static class ChampionTitleSystem public static void Configure() { - GenericPersistence.Register("ChampionTitles", Serialize, Deserialize); + _championTitlePersistence = new ChampionTitleSystem(); } public static void Initialize() @@ -33,7 +35,11 @@ public static class ChampionTitleSystem } } - private static void Deserialize(IGenericReader reader) + public ChampionTitleSystem() : base("ChampionTitles", 10) + { + } + + public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); @@ -47,7 +53,7 @@ public static class ChampionTitleSystem } } - private static void Serialize(IGenericWriter writer) + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version @@ -59,10 +65,10 @@ public static class ChampionTitleSystem } } - public static bool GetChampionTitleContext(this PlayerMobile player, out ChampionTitleContext context) => + public static bool GetChampionTitleContext(PlayerMobile player, out ChampionTitleContext context) => _championTitleContexts.TryGetValue(player, out context); - public static ChampionTitleContext GetOrCreateChampionTitleContext(this PlayerMobile player) + public static ChampionTitleContext GetOrCreateChampionTitleContext(PlayerMobile player) { ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_championTitleContexts, player, out var exists); if (!exists) @@ -76,7 +82,7 @@ public static class ChampionTitleSystem // Called when killing a harrower. Will give a minimum of 1 point. public static void AwardHarrowerTitle(PlayerMobile pm) { - var context = pm.GetOrCreateChampionTitleContext(); + var context = GetOrCreateChampionTitleContext(pm); var count = 1; for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++) @@ -91,9 +97,9 @@ public static class ChampionTitleSystem context.Harrower = Math.Max(count, context.Harrower); // Harrower titles never decay. } - public static int GetChampionTitleLabel(this PlayerMobile player) + public static int GetChampionTitleLabel(PlayerMobile player) { - if (!player.GetChampionTitleContext(out var context)) + if (!GetChampionTitleContext(player, out var context)) { return 0; } diff --git a/Projects/UOContent/Engines/Factions/Core/FactionSystem.cs b/Projects/UOContent/Engines/Factions/Core/FactionSystem.cs index 13a8a6df4..8bfb25353 100644 --- a/Projects/UOContent/Engines/Factions/Core/FactionSystem.cs +++ b/Projects/UOContent/Engines/Factions/Core/FactionSystem.cs @@ -2,8 +2,9 @@ using System; namespace Server.Factions; -public static class FactionSystem +public class FactionSystem : GenericPersistence { + private static FactionSystem _factionSystem; public static bool Enabled { get; private set; } public static void Configure() @@ -12,10 +13,14 @@ public static class FactionSystem if (Enabled) { - GenericPersistence.Register("Factions", Serialize, Deserialize); + _factionSystem = new(); } } + public FactionSystem() : base("Factions", 10) + { + } + // This does not do the actual work of removing faction stuff, only turns off the persistence. public static void Disable() { @@ -24,7 +29,7 @@ public static class FactionSystem return; } - Persistence.Unregister("Factions"); + _factionSystem.Unregister(); Enabled = false; ServerConfiguration.SetSetting("factions.enabled", false); } @@ -37,12 +42,12 @@ public static class FactionSystem return; } - GenericPersistence.Register("Factions", Serialize, Deserialize); + _factionSystem.Register(); Enabled = true; ServerConfiguration.SetSetting("factions.enabled", true); } - private static void Serialize(IGenericWriter writer) + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version @@ -59,7 +64,7 @@ public static class FactionSystem } } - private static void Deserialize(IGenericReader reader) + public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); diff --git a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs index d997a11f2..349d98e4d 100644 --- a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs +++ b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs @@ -8,8 +8,10 @@ using Server.Mobiles; namespace Server.Engines.PlayerMurderSystem; -public static class PlayerMurderSystem +public class PlayerMurderSystem : GenericPersistence { + private static PlayerMurderSystem _playerMurderPersistence; + private static readonly ILogger logger = LogFactory.GetLogger(typeof(PlayerMurderSystem)); // All of the players with murders @@ -28,10 +30,10 @@ public static class PlayerMurderSystem public static void Configure() { - GenericPersistence.Register("PlayerMurders", Serialize, Deserialize); - _shortTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.shortTermMurderDuration", TimeSpan.FromHours(8)); _longTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.longTermMurderDuration", TimeSpan.FromHours(40)); + + _playerMurderPersistence = new PlayerMurderSystem(); } public static void Initialize() @@ -49,6 +51,10 @@ public static class PlayerMurderSystem } } + public PlayerMurderSystem() : base("PlayerMurders", 10) + { + } + // Only used for migrations! public static void MigrateContext(PlayerMobile player, TimeSpan shortTerm, TimeSpan longTerm) { @@ -61,7 +67,7 @@ public static class PlayerMurderSystem return; } - var context = player.GetOrCreateMurderContext(); + var context = GetOrCreateMurderContext(player); // We make a big assumption that by the time this is called, the Mobile/PlayerMobile info is deserialized if (Mobile.MurderMigrations?.TryGetValue(player, out var shortTermMurders) == true) @@ -100,7 +106,7 @@ public static class PlayerMurderSystem } } - private static void Deserialize(IGenericReader reader) + public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); @@ -114,7 +120,7 @@ public static class PlayerMurderSystem } } - private static void Serialize(IGenericWriter writer) + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version @@ -126,10 +132,10 @@ public static class PlayerMurderSystem } } - public static bool GetMurderContext(this PlayerMobile player, out MurderContext context) => + public static bool GetMurderContext(PlayerMobile player, out MurderContext context) => _murderContexts.TryGetValue(player, out context); - public static MurderContext GetOrCreateMurderContext(this PlayerMobile player) + public static MurderContext GetOrCreateMurderContext(PlayerMobile player) { ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_murderContexts, player, out var exists); if (!exists) @@ -142,14 +148,14 @@ public static class PlayerMurderSystem public static void ManuallySetShortTermMurders(PlayerMobile player, int shortTermMurders) { - var context = player.GetOrCreateMurderContext(); + var context = GetOrCreateMurderContext(player); context.ShortTermMurders = shortTermMurders; UpdateMurderContext(context); } public static void OnPlayerMurder(PlayerMobile player) { - var context = player.GetOrCreateMurderContext(); + var context = GetOrCreateMurderContext(player); context.ShortTermMurders++; player.Kills++; diff --git a/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs b/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs index 5b2112386..80d4fcdd4 100644 --- a/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs +++ b/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs @@ -6,10 +6,11 @@ using Server.Utilities; namespace Server.Engines.Stealables; -public static class StealableArtifacts +public class StealableArtifacts : GenericPersistence { private static readonly ILogger logger = LogFactory.GetLogger(typeof(StealableArtifacts)); + private static StealableArtifacts _stealableArtifactsPersistence; private static bool _enabled; private static Type[] _typesOfEntries; private static StealableInstance[] _artifacts; @@ -19,7 +20,11 @@ public static class StealableArtifacts public static void Configure() { - GenericPersistence.Register("StealableArtifacts", Serialize, Deserialize); + _stealableArtifactsPersistence = new StealableArtifacts(); + } + + public StealableArtifacts() : base("StealableArtifacts", 10) + { } private static void RemoveStealableArtifacts() @@ -242,7 +247,7 @@ public static class StealableArtifacts } } - private static void Serialize(IGenericWriter writer) + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(1); // version @@ -262,7 +267,7 @@ public static class StealableArtifacts } } - private static void Deserialize(IGenericReader reader) + public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); @@ -441,7 +446,7 @@ public static class StealableArtifacts { base.Deserialize(reader); - StealableArtifacts.Deserialize(reader); + _stealableArtifactsPersistence.Deserialize(reader); Timer.DelayCall(Delete); } diff --git a/Projects/UOContent/Engines/Virtues/Compassion.cs b/Projects/UOContent/Engines/Virtues/Compassion.cs index a1131662d..be8f78faf 100644 --- a/Projects/UOContent/Engines/Virtues/Compassion.cs +++ b/Projects/UOContent/Engines/Virtues/Compassion.cs @@ -24,7 +24,7 @@ public static class CompassionVirtue public static void CheckAtrophy(PlayerMobile pm) { - var virtues = pm.GetVirtues(); + var virtues = VirtueSystem.GetVirtues(pm); if (virtues?.Compassion > 0 && CanAtrophy(virtues)) { if (VirtueSystem.Atrophy(pm, VirtueName.Compassion, LossAmount)) diff --git a/Projects/UOContent/Engines/Virtues/Honor.cs b/Projects/UOContent/Engines/Virtues/Honor.cs index a5ae4c554..3ce8064a8 100644 --- a/Projects/UOContent/Engines/Virtues/Honor.cs +++ b/Projects/UOContent/Engines/Virtues/Honor.cs @@ -39,7 +39,7 @@ public static class HonorVirtue private static void EmbraceHonor(PlayerMobile pm) { - var virtues = pm.GetVirtues(); + var virtues = VirtueSystem.GetVirtues(pm); if (virtues?.HonorActive == true) { @@ -73,7 +73,7 @@ public static class HonorVirtue public static void ActivateEmbrace(PlayerMobile pm) { var duration = GetHonorDuration(pm); - var virtues = pm.GetOrCreateVirtues(); + var virtues = VirtueSystem.GetOrCreateVirtues(pm); int usedPoints = virtues.Honor switch { @@ -92,7 +92,7 @@ public static class HonorVirtue (m) => { // We get the virtues again, in case it was deleted/dereferenced - var v = m.GetOrCreateVirtues(); + var v = VirtueSystem.GetOrCreateVirtues(m); v.HonorActive = false; v.LastHonorUse = Core.Now; m.SendLocalizedMessage(1063236); // You no longer embrace your honor diff --git a/Projects/UOContent/Engines/Virtues/HonorContext.cs b/Projects/UOContent/Engines/Virtues/HonorContext.cs index 2865a213e..8eab8bd1d 100644 --- a/Projects/UOContent/Engines/Virtues/HonorContext.cs +++ b/Projects/UOContent/Engines/Virtues/HonorContext.cs @@ -186,7 +186,7 @@ public class HonorContext Source.Mana += restore; } - if (Source.GetVirtues().Honor > targetFame) + if (VirtueSystem.GetVirtues(Source).Honor > targetFame) { return; } diff --git a/Projects/UOContent/Engines/Virtues/Justice.cs b/Projects/UOContent/Engines/Virtues/Justice.cs index 36e509fee..c24017962 100644 --- a/Projects/UOContent/Engines/Virtues/Justice.cs +++ b/Projects/UOContent/Engines/Virtues/Justice.cs @@ -32,23 +32,23 @@ public class JusticeVirtue } public static bool IsProtected(PlayerMobile pm) => - pm.GetVirtues() is { JusticeStatus: JusticeProtectorStatus.Protected, JusticeProtection: not null }; + VirtueSystem.GetVirtues(pm) is { JusticeStatus: JusticeProtectorStatus.Protected, JusticeProtection: not null }; public static PlayerMobile GetProtector(PlayerMobile pm) => - pm.GetVirtues() is { JusticeStatus: JusticeProtectorStatus.Protected } virtues ? virtues.JusticeProtection : null; + VirtueSystem.GetVirtues(pm) is { JusticeStatus: JusticeProtectorStatus.Protected } virtues ? virtues.JusticeProtection : null; public static PlayerMobile GetProtected(PlayerMobile pm) => - pm.GetVirtues() is { JusticeStatus: JusticeProtectorStatus.Protector } virtues ? virtues.JusticeProtection : null; + VirtueSystem.GetVirtues(pm) is { JusticeStatus: JusticeProtectorStatus.Protector } virtues ? virtues.JusticeProtection : null; public static void CancelProtection(PlayerMobile pm) { - if (pm.GetVirtues() is { JusticeStatus: not JusticeProtectorStatus.None } virtues) + if (VirtueSystem.GetVirtues(pm) is { JusticeStatus: not JusticeProtectorStatus.None } virtues) { var protector = virtues.JusticeProtection; virtues.JusticeProtection = null; virtues.JusticeStatus = JusticeProtectorStatus.None; - virtues = protector?.GetVirtues(); + virtues = VirtueSystem.GetVirtues(protector); if (virtues != null) { virtues.JusticeProtection = null; @@ -59,13 +59,13 @@ public class JusticeVirtue public static bool CancelProtection(PlayerMobile pm, out PlayerMobile protector) { - if (pm.GetVirtues() is { JusticeStatus: not JusticeProtectorStatus.None } virtues) + if (VirtueSystem.GetVirtues(pm) is { JusticeStatus: not JusticeProtectorStatus.None } virtues) { protector = virtues.JusticeProtection; virtues.JusticeProtection = null; virtues.JusticeStatus = JusticeProtectorStatus.None; - virtues = protector?.GetVirtues(); + virtues = VirtueSystem.GetVirtues(protector); if (virtues != null) { virtues.JusticeProtection = null; @@ -255,7 +255,7 @@ public class JusticeVirtue public static void CheckAtrophy(PlayerMobile pm) { - var virtues = pm.GetVirtues(); + var virtues = VirtueSystem.GetVirtues(pm); if (virtues?.Justice > 0 && CanAtrophy(virtues)) { if (VirtueSystem.Atrophy(pm, VirtueName.Justice, LossAmount)) diff --git a/Projects/UOContent/Engines/Virtues/Sacrifice.cs b/Projects/UOContent/Engines/Virtues/Sacrifice.cs index 29d09f55a..fd2d91d1c 100644 --- a/Projects/UOContent/Engines/Virtues/Sacrifice.cs +++ b/Projects/UOContent/Engines/Virtues/Sacrifice.cs @@ -35,7 +35,7 @@ public static class SacrificeVirtue public static void CheckAtrophy(PlayerMobile pm) { - var virtues = pm.GetVirtues(); + var virtues = VirtueSystem.GetVirtues(pm); if (virtues?.Sacrifice > 0 && CanAtrophy(virtues)) { if (VirtueSystem.Atrophy(pm, VirtueName.Sacrifice, LossAmount)) @@ -67,7 +67,7 @@ public static class SacrificeVirtue } else { - var virtues = from.GetVirtues(); + var virtues = VirtueSystem.GetVirtues(from); if (virtues?.AvailableResurrects > 0) { /* @@ -124,7 +124,7 @@ public static class SacrificeVirtue } else { - var virtues = from.GetOrCreateVirtues(); + var virtues = VirtueSystem.GetOrCreateVirtues(from); if (!CanGain(virtues)) { from.SendLocalizedMessage(1052016); // You must wait approximately one day before sacrificing again. diff --git a/Projects/UOContent/Engines/Virtues/Valor.cs b/Projects/UOContent/Engines/Virtues/Valor.cs index 4c9d58b10..affc4e842 100644 --- a/Projects/UOContent/Engines/Virtues/Valor.cs +++ b/Projects/UOContent/Engines/Virtues/Valor.cs @@ -30,7 +30,7 @@ public static class ValorVirtue public static void CheckAtrophy(PlayerMobile pm) { - var virtues = pm.GetVirtues(); + var virtues = VirtueSystem.GetVirtues(pm); if (virtues?.Valor > 0 && CanAtrophy(virtues)) { if (VirtueSystem.Atrophy(pm, VirtueName.Valor, LossAmount)) @@ -90,7 +90,7 @@ public static class ValorVirtue } } - if (from.GetVirtues()?.GetValue((int)VirtueName.Valor) >= needed) + if (VirtueSystem.GetVirtues(from)?.GetValue((int)VirtueName.Valor) >= needed) { VirtueSystem.Atrophy(from, VirtueName.Valor, consumed); // Your challenge is heard by the Champion of this region! Beware its wrath! diff --git a/Projects/UOContent/Engines/Virtues/VirtueGump.cs b/Projects/UOContent/Engines/Virtues/VirtueGump.cs index d7e0e93e4..98179b464 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueGump.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueGump.cs @@ -113,7 +113,7 @@ public class VirtueGump : Gump private int GetHueFor(int index) { - var value = _beheld.GetVirtues()?.GetValue(index) ?? 0; + var value = VirtueSystem.GetVirtues((_beheld))?.GetValue(index) ?? 0; if (value < 4000) { diff --git a/Projects/UOContent/Engines/Virtues/VirtueInfoGump.cs b/Projects/UOContent/Engines/Virtues/VirtueInfoGump.cs index 47f6a1269..6e29a53df 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueInfoGump.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueInfoGump.cs @@ -18,7 +18,7 @@ public class VirtueInfoGump : Gump _desc = description; _site = webPage; - var value = beholder.GetVirtues()?.GetValue((int)virtue) ?? 0; + var value = VirtueSystem.GetVirtues(beholder)?.GetValue((int)virtue) ?? 0; AddPage(0); diff --git a/Projects/UOContent/Engines/Virtues/VirtueStatusGump.cs b/Projects/UOContent/Engines/Virtues/VirtueStatusGump.cs index 15dcea67c..4cb927304 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueStatusGump.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueStatusGump.cs @@ -81,7 +81,7 @@ public class VirtueStatusGump : Gump _beholder, virtue, GetVirtueDescription(virtue), - @$"https://uo.com/wiki/ultima-online-wiki/gameplay/the-virtues/#{virtue.GetLowerCaseName()}" + @$"https://uo.com/wiki/ultima-online-wiki/gameplay/the-virtues/#{VirtueSystem.GetLowerCaseName(virtue)}" )); } } diff --git a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs index 12bde4fb3..6ea36c3d9 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs @@ -28,12 +28,23 @@ public enum VirtueName Honesty } -public static class VirtueSystem +public class VirtueSystem : GenericPersistence { private static readonly ILogger logger = LogFactory.GetLogger(typeof(VirtueSystem)); private static readonly Dictionary _playerVirtues = new(); + private static VirtueSystem _virtueSystemPersistence; + + public static void Configure() + { + _virtueSystemPersistence = new VirtueSystem(); + } + + public VirtueSystem() : base("Virtues", 10) + { + } + private static void FixVirtue(Mobile m, int[] virtueValues) { if (m is not PlayerMobile pm) @@ -52,11 +63,6 @@ public static class VirtueSystem } } - public static void Configure() - { - GenericPersistence.Register("Virtues", Serialize, Deserialize); - } - public static void Initialize() { var migrations = Mobile.VirtueMigrations; @@ -69,7 +75,7 @@ public static class VirtueSystem } } - private static void Serialize(IGenericWriter writer) + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version @@ -81,7 +87,7 @@ public static class VirtueSystem } } - private static void Deserialize(IGenericReader reader) + public override void Deserialize(IGenericReader reader) { reader.ReadEncodedInt(); // version @@ -99,10 +105,10 @@ public static class VirtueSystem } } - public static VirtueContext GetVirtues(this PlayerMobile from) => + public static VirtueContext GetVirtues(PlayerMobile from) => _playerVirtues.TryGetValue(from, out var context) ? context : null; - public static VirtueContext GetOrCreateVirtues(this PlayerMobile from) + public static VirtueContext GetOrCreateVirtues(PlayerMobile from) { ref VirtueContext context = ref CollectionsMarshal.GetValueRefOrAddDefault(_playerVirtues, from, out bool exists); if (!exists) @@ -114,11 +120,11 @@ public static class VirtueSystem } public static bool IsHighestPath(PlayerMobile from, VirtueName virtue) => - from.GetVirtues()?.GetValue((int)virtue) >= GetMaxAmount(virtue); + GetVirtues(from)?.GetValue((int)virtue) >= GetMaxAmount(virtue); public static VirtueLevel GetLevel(Mobile from, VirtueName virtue) { - var v = (from as PlayerMobile)?.GetVirtues()?.GetValue((int)virtue) ?? 0; + var v = GetVirtues(from as PlayerMobile)?.GetValue((int)virtue) ?? 0; int vl; if (v < 4000) @@ -137,7 +143,7 @@ public static class VirtueSystem return (VirtueLevel)vl; } - public static string GetName(this VirtueName virtue) => + public static string GetName(VirtueName virtue) => virtue switch { VirtueName.Humility => "Humility", @@ -151,7 +157,7 @@ public static class VirtueSystem _ => "" }; - public static string GetLowerCaseName(this VirtueName virtue) => + public static string GetLowerCaseName(VirtueName virtue) => virtue switch { VirtueName.Humility => "humility", @@ -240,7 +246,7 @@ public static class VirtueSystem public static bool Atrophy(PlayerMobile from, VirtueName virtue, int amount = 1) { - var virtues = from.GetVirtues(); + var virtues = GetVirtues(from); if (virtues == null) { return false; @@ -268,7 +274,7 @@ public static class VirtueSystem public static void AwardVirtue(PlayerMobile pm, VirtueName virtue, int amount) { - var virtues = pm.GetOrCreateVirtues(); + var virtues = GetOrCreateVirtues(pm); if (virtue == VirtueName.Compassion) { if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay) @@ -285,7 +291,7 @@ public static class VirtueSystem } var gainedPath = false; - var virtueName = virtue.GetName(); + var virtueName = GetName(virtue); if (Award(pm, virtue, amount, ref gainedPath)) { @@ -331,7 +337,7 @@ public static class VirtueSystem } } - public static void CheckAtrophies(this PlayerMobile pm) + public static void CheckAtrophies(PlayerMobile pm) { SacrificeVirtue.CheckAtrophy(pm); JusticeVirtue.CheckAtrophy(pm); diff --git a/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs b/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs index bdf385050..c30ebce2e 100644 --- a/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs +++ b/Projects/UOContent/Items/Skill Items/Thief/DisguisePersistence.cs @@ -4,16 +4,21 @@ using Server.Mobiles; namespace Server.Items; -public static class DisguisePersistence +public class DisguisePersistence : GenericPersistence { + private static DisguisePersistence _disguisePersistence; public static Dictionary Timers { get; } = new(); public static void Configure() { - GenericPersistence.Register("Disguises", Serialize, Deserialize); + _disguisePersistence = new DisguisePersistence(); } - private static void Deserialize(IGenericReader reader) + public DisguisePersistence() : base("Disguises", 10) + { + } + + public override void Deserialize(IGenericReader reader) { var count = reader.ReadEncodedInt(); for (var i = 0; i < count; ++i) @@ -24,7 +29,7 @@ public static class DisguisePersistence } } - private static void Serialize(IGenericWriter writer) + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(Timers.Count); foreach (var (m, timer) in Timers) diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index 8a9eca68c..dfce268ea 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -1857,7 +1857,7 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab if (attacker is PlayerMobile pmAttacker && !(Core.ML && defender is PlayerMobile)) { - if (pmAttacker.GetVirtues()?.HonorActive == true && pmAttacker.InRange(defender, 1)) + if (VirtueSystem.GetVirtues(pmAttacker)?.HonorActive == true && pmAttacker.InRange(defender, 1)) { percentageBonus += 25; } diff --git a/Projects/UOContent/Misc/StaminaSystem.cs b/Projects/UOContent/Misc/StaminaSystem.cs index a497b1573..c13fe15cb 100644 --- a/Projects/UOContent/Misc/StaminaSystem.cs +++ b/Projects/UOContent/Misc/StaminaSystem.cs @@ -15,7 +15,7 @@ public enum DFAlgorithm PainSpike } -public static class StaminaSystem +public class StaminaSystem : GenericPersistence { private static readonly ILogger logger = LogFactory.GetLogger(typeof(StaminaSystem)); @@ -44,11 +44,13 @@ public static class StaminaSystem AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10); EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true); UseMountStaminaOnlyWhenOverloaded = ServerConfiguration.GetSetting("stamina.useMountStaminaOnlyWhenOverloaded", Core.SA); - - GenericPersistence.Register("StaminaSystem", Serialize, Deserialize); } - private static void Serialize(IGenericWriter writer) + public StaminaSystem() : base("StaminaSystem", 10) + { + } + + public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version @@ -60,7 +62,7 @@ public static class StaminaSystem } } - private static void Deserialize(IGenericReader reader) + public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); diff --git a/Projects/UOContent/Misc/Titles.cs b/Projects/UOContent/Misc/Titles.cs index 8c002515b..076ae9a05 100644 --- a/Projects/UOContent/Misc/Titles.cs +++ b/Projects/UOContent/Misc/Titles.cs @@ -321,7 +321,7 @@ namespace Server.Misc if (beheld is PlayerMobile mobile && mobile.DisplayChampionTitle) { - var titleLabel = mobile.GetChampionTitleLabel(); + var titleLabel = ChampionTitleSystem.GetChampionTitleLabel(mobile); if (titleLabel > 0) { // Should this be translated to the receivers language? Prefix titles aren't? diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 69489d798..184b2f42b 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -2702,7 +2702,7 @@ public abstract class BaseAI } // Ignore players with activated honor - if (m_Mobile.Combatant != m && pm?.GetVirtues()?.HonorActive == true) + if (m_Mobile.Combatant != m && VirtueSystem.GetVirtues(pm)?.HonorActive == true) { continue; } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index b81e5bdbb..3a2ccd82b 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -1288,7 +1288,7 @@ namespace Server.Mobiles return false; } - if ((m as PlayerMobile)?.GetVirtues()?.HonorActive == true) + if (VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true) { return false; } diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 06b80d7b3..ad7836ba4 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -697,30 +697,32 @@ namespace Server.Mobiles } [CommandProperty(AccessLevel.GameMaster, canModify: true)] - public ChampionTitleContext ChampionTitles => this.GetOrCreateChampionTitleContext(); + public ChampionTitleContext ChampionTitles => ChampionTitleSystem.GetOrCreateChampionTitleContext(this); [CommandProperty(AccessLevel.GameMaster)] public int ShortTermMurders { - get => this.GetMurderContext(out var context) ? context.ShortTermMurders : 0; + get => PlayerMurderSystem.GetMurderContext(this, out var context) ? context.ShortTermMurders : 0; set => PlayerMurderSystem.ManuallySetShortTermMurders(this, value); } [CommandProperty(AccessLevel.GameMaster)] - public DateTime ShortTermMurderExpiration => this.GetMurderContext(out var context) && context.ShortTermMurders > 0 - ? Core.Now + (context.ShortTermElapse - GameTime) - : DateTime.MinValue; + public DateTime ShortTermMurderExpiration + => PlayerMurderSystem.GetMurderContext(this, out var context) && context.ShortTermMurders > 0 + ? Core.Now + (context.ShortTermElapse - GameTime) + : DateTime.MinValue; [CommandProperty(AccessLevel.GameMaster)] - public DateTime LongTermMurderExpiration => Kills > 0 && this.GetMurderContext(out var context) - ? Core.Now + (context.LongTermElapse - GameTime) - : DateTime.MinValue; + public DateTime LongTermMurderExpiration + => Kills > 0 && PlayerMurderSystem.GetMurderContext(this, out var context) + ? Core.Now + (context.LongTermElapse - GameTime) + : DateTime.MinValue; [CommandProperty(AccessLevel.GameMaster)] public int KnownRecipes => m_AcquiredRecipes?.Count ?? 0; [CommandProperty(AccessLevel.Counselor, canModify: true)] - public VirtueContext Virtues => this.GetOrCreateVirtues(); + public VirtueContext Virtues => VirtueSystem.GetOrCreateVirtues(this); public HonorContext ReceivedHonorContext { get; set; } @@ -1255,7 +1257,7 @@ namespace Server.Mobiles if (from is PlayerMobile mobile) { - mobile.CheckAtrophies(); + VirtueSystem.CheckAtrophies(mobile); mobile.ClaimAutoStabledPets(); } } @@ -3426,7 +3428,7 @@ namespace Server.Mobiles // https://uo.com/wiki/ultima-online-wiki/player/skill-titles-order/ if (DisplayChampionTitle) { - var titleLabel = this.GetChampionTitleLabel(); + var titleLabel = ChampionTitleSystem.GetChampionTitleLabel(this); if (titleLabel > 0) { list.Add(titleLabel); diff --git a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs index be7a067fa..1b9851c31 100644 --- a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs +++ b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs @@ -643,7 +643,7 @@ public partial class BaseEscortable : BaseCreature if (escorter is PlayerMobile pm) { - var virtues = pm.GetOrCreateVirtues(); + var virtues = VirtueSystem.GetOrCreateVirtues(pm); if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay) { virtues.NextCompassionDay = DateTime.MinValue; diff --git a/Projects/UOContent/Skills/AnimalTaming.cs b/Projects/UOContent/Skills/AnimalTaming.cs index fd604f944..7e661f692 100644 --- a/Projects/UOContent/Skills/AnimalTaming.cs +++ b/Projects/UOContent/Skills/AnimalTaming.cs @@ -228,7 +228,7 @@ namespace Server.SkillHandlers creature.AIObject?.DoMove(creature.Direction); if (from is PlayerMobile pm && - !(pm.GetVirtues()?.HonorActive == true || + !(VirtueSystem.GetVirtues(pm)?.HonorActive == true || TransformationSpellHelper.UnderTransformation(pm, typeof(EtherealVoyageSpell)))) { creature.Combatant = pm;