diff --git a/Projects/Server/IAccount.cs b/Projects/Server/IAccount.cs index 6c6b1d317..2efd50439 100644 --- a/Projects/Server/IAccount.cs +++ b/Projects/Server/IAccount.cs @@ -88,7 +88,7 @@ namespace Server.Accounting long GetTotalGold(); } - public interface IAccount : IGoldAccount, IComparable + public interface IAccount : IGoldAccount, IComparable, ISerializable { string Username { get; set; } string Email { get; set; } diff --git a/Projects/Server/Network/Pipe.cs b/Projects/Server/Network/Pipe.cs index b829e51a4..d8cbfe290 100644 --- a/Projects/Server/Network/Pipe.cs +++ b/Projects/Server/Network/Pipe.cs @@ -141,7 +141,7 @@ namespace Server.Network { if (_pipe._writeAwaitBeginning) { - throw new Exception("Double await on reader"); + throw new Exception("Double await on writer"); } return this; diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs new file mode 100644 index 000000000..fdc4ec321 --- /dev/null +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -0,0 +1,54 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: GenericPersistence.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.IO; + +namespace Server +{ + public static class GenericPersistence + { + public static void Serialize(Action serializer) => serializer(new BufferWriter(true)); + + public static void WriteSnapshot(string path, Action serializer) + { + AssemblyHandler.EnsureDirectory(Path.GetDirectoryName(path)); + + using var fs = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None); + serializer(new BinaryFileWriter(fs, true)); + } + + public static void Deserialize(string path, Action deserializer, bool ensure = true) + { + AssemblyHandler.EnsureDirectory(Path.GetDirectoryName(path)); + + if (!File.Exists(path)) + { + if (ensure) + { + new FileInfo(path).Create().Close(); + } + + return; + } + + using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + // TODO: Support files larger than 2GB + var buffer = GC.AllocateUninitializedArray((int)fs.Length); + + deserializer(new BufferReader(buffer)); + } + } +} diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index 426c7b734..2d1b05674 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -14,57 +14,103 @@ *************************************************************************/ using System; +using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; namespace Server { public static class Persistence { - public static void Serialize(string path, Action serializer) + public static readonly SortedSet _registry = new(new RegistryEntryComparer()); + + public static void Register( + Action serializer, + Action snapshotWriter, + Action deserializer, + int priority = 100 + ) { - AssemblyHandler.EnsureDirectory(Path.GetDirectoryName(path)); + _registry.Add( + new RegistryEntry + { + Priority = priority, + Serialize = serializer, + WriteSnapshot = snapshotWriter, + Deserialize = deserializer + } + ); + } - using var fs = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None); - var writer = new BinaryFileWriter(fs, true); - - try + public static void Load(string path) + { + // This should probably not be parallel since Mobiles must be loaded before Items + foreach (var entry in _registry) { - serializer(writer); - } - catch (Exception e) - { - Console.WriteLine("[Persistence]: Failed to serialize"); - Console.WriteLine(e); + entry.Deserialize(path); } } - public static void Deserialize(string path, Action deserializer, bool ensure = true) + public static void Serialize() { - AssemblyHandler.EnsureDirectory(Path.GetDirectoryName(path)); + Parallel.ForEach(_registry, entry => entry.Serialize()); + } - if (!File.Exists(path)) + public static void WriteSnapshot(string path) + { + foreach (var entry in _registry) { - if (ensure) - { - new FileInfo(path).Create().Close(); - } - - return; + entry.WriteSnapshot(path); } + } - using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - // TODO: Support files larger than 2GB - var buffer = GC.AllocateUninitializedArray((int)fs.Length); + public class RegistryEntry + { + public int Priority { get; init; } + public Action Serialize { get; init; } // Serializing to memory buffers + public Action WriteSnapshot { get; init; } + public Action Deserialize { get; init; } + } + internal class RegistryEntryComparer : IComparer + { + public int Compare(RegistryEntry x, RegistryEntry y) => + x?.Priority.CompareTo(y?.Priority) ?? 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteConsole(string message) + { + var now = DateTime.UtcNow; + Console.Write("[{0} {1}] Persistence: {2}", now.ToShortDateString(), now.ToLongTimeString(), message); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteConsoleLine(string message) + { + var now = DateTime.UtcNow; + Console.WriteLine("[{0} {1}] Persistence: {2}", now.ToShortDateString(), now.ToLongTimeString(), message); + } + + public static void TraceException(Exception ex) + { try { - deserializer(new BufferReader(buffer)); + using var op = new StreamWriter("save-errors.log", true); + op.WriteLine("# {0}", DateTime.UtcNow); + + op.WriteLine(ex); + + op.WriteLine(); + op.WriteLine(); } - catch (Exception e) + catch { - Console.WriteLine("[Persistence]: Failed to deserialize"); - Console.WriteLine(e); + // ignored } + + Console.WriteLine(ex); } } } diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs new file mode 100644 index 000000000..8899b6661 --- /dev/null +++ b/Projects/Server/World/EntityPersistence.cs @@ -0,0 +1,285 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: EntityPersistence.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.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Server +{ + public static class EntityPersistence + { + public static void WriteEntities( + IIndexInfo indexInfo, + Dictionary entities, + List types, + string savePath, + out Dictionary counts + ) where T : class, ISerializable + { + counts = new Dictionary(); + + var typeName = indexInfo.TypeName; + + var path = Path.Combine(savePath, typeName); + + AssemblyHandler.EnsureDirectory(path); + + string idxPath = Path.Combine(path, $"{typeName}.idx"); + string tdbPath = Path.Combine(path, $"{typeName}.tdb"); + string binPath = Path.Combine(path, $"{typeName}.bin"); + + using var idx = new BinaryFileWriter(idxPath, false); + using var tdb = new BinaryFileWriter(tdbPath, false); + using var bin = new BinaryFileWriter(binPath, true); + + idx.Write(entities.Count); + foreach (var e in entities.Values) + { + long start = bin.Position; + + idx.Write(e.TypeRef); + idx.Write(e.Serial); + idx.Write(start); + + e.SerializeTo(bin); + + idx.Write((int)(bin.Position - start)); + + var type = e.GetType().FullName; + if (type != null) + { + counts[type] = (counts.TryGetValue(type, out var count) ? count : 0) + 1; + } + } + + tdb.Write(types.Count); + for (int i = 0; i < types.Count; ++i) + { + tdb.Write(types[i].FullName); + } + } + + [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, + out List> entities + ) where T : class, ISerializable + { + var map = new Dictionary(); + object[] ctorArgs = new object[1]; + + var indexType = indexInfo.TypeName; + + string indexPath = Path.Combine(path, indexType, $"{indexType}.idx"); + string typesPath = Path.Combine(path, indexType, $"{indexType}.tdb"); + + entities = new List>(); + + if (!File.Exists(indexPath) || !File.Exists(typesPath)) + { + return map; + } + + using FileStream idx = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read); + BinaryReader idxReader = new BinaryReader(idx); + + using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read); + BinaryReader tdbReader = new BinaryReader(tdb); + + List> types = ReadTypes(tdbReader); + + var count = idxReader.ReadInt32(); + + for (int i = 0; i < count; ++i) + { + var typeID = idxReader.ReadInt32(); + var number = idxReader.ReadUInt32(); + var pos = idxReader.ReadInt64(); + var length = idxReader.ReadInt32(); + + Tuple objs = types[typeID]; + + if (objs == null) + { + continue; + } + + T t; + ConstructorInfo ctor = objs.Item1; + I indexer = indexInfo.CreateIndex(number); + + ctorArgs[0] = indexer; + t = ctor.Invoke(ctorArgs) as T; + + if (t != null) + { + entities.Add(new EntityIndex(t, typeID, pos, length)); + map[indexer] = t; + } + } + + tdbReader.Close(); + idxReader.Close(); + + return map; + } + + public static void LoadData( + string path, + IIndexInfo indexInfo, + List> entities + ) where T : class, ISerializable + { + var indexType = indexInfo.TypeName; + + string dataPath = Path.Combine(path, indexType, $"{indexType}.bin"); + + if (!File.Exists(dataPath)) + { + return; + } + + using FileStream bin = new FileStream(dataPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + BufferReader br = null; + + foreach (var entry in entities) + { + T t = entry.Entity; + + // Skip this entry + if (t == null) + { + bin.Seek(entry.Length, SeekOrigin.Current); + continue; + } + + var buffer = GC.AllocateUninitializedArray(entry.Length); + if (br == null) + { + br = new BufferReader(buffer); + } + else + { + br.SwapBuffers(buffer, out _); + } + + bin.Read(buffer.AsSpan()); + string error; + + try + { + t.Deserialize(br); + + error = br.Position != entry.Length + ? $"Serialized object was {entry.Length} bytes, but {br.Position} bytes deserialized" + : null; + } + catch (Exception e) + { + error = e.ToString(); + } + + if (error == null) + { + t.InitializeSaveBuffer(buffer); + } + else + { + Utility.PushColor(ConsoleColor.Red); + Persistence.WriteConsoleLine($"***** Bad deserialize of {t.GetType()} *****"); + Persistence.WriteConsoleLine(error); + Utility.PopColor(); + + Persistence.WriteConsoleLine("Delete the object and continue? (y/n)"); + + if (Console.ReadKey(true).Key != ConsoleKey.Y) + { + throw new Exception("Deserialization failed."); + } + t.Delete(); + } + } + } + + private static List> ReadTypes(BinaryReader tdbReader) + { + var constructorTypes = new[] { typeof(I) }; + + var count = tdbReader.ReadInt32(); + + var types = new List>(count); + + for (var i = 0; i < count; ++i) + { + var typeName = tdbReader.ReadString(); + + var t = AssemblyHandler.FindTypeByFullName(typeName, false); + + if (t?.IsAbstract != false) + { + Persistence.WriteConsoleLine("failed"); + + var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; + + Persistence.WriteConsoleLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)"); + + if (Console.ReadKey(true).Key == ConsoleKey.Y) + { + types.Add(null); + Persistence.WriteConsole("Loading..."); + continue; + } + + Persistence.WriteConsoleLine("Types will not be deleted. An exception will be thrown."); + + throw new Exception($"Bad type '{typeName}'"); + } + + var ctor = t.GetConstructor(constructorTypes); + + if (ctor != null) + { + types.Add(new Tuple(ctor, typeName)); + } + else + { + throw new Exception($"Type '{t}' does not have a serialization constructor"); + } + } + + return types; + } + + private static void SerializeTo(this ISerializable entity, IGenericWriter writer) + { + var saveBuffer = entity.SaveBuffer; + writer.Write(saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position)); + + // Resize to exact buffer size + entity.SaveBuffer.Resize((int)entity.SaveBuffer.Position); + } + } +} diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index c2f7101ca..08244d57a 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -19,10 +19,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; using Server.Guilds; using Server.Network; @@ -141,6 +139,9 @@ namespace Server _tempSavePath = Path.Combine(Core.BaseDirectory, tempSavePath); var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves"); _savePath = Path.Combine(Core.BaseDirectory, savePath); + + // Mobiles & Items + Persistence.Register(SaveEntities, WriteEntities, LoadEntities, 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -220,188 +221,19 @@ namespace Server public static void Broadcast(int hue, bool ascii, string format, params object[] args) => Broadcast(hue, ascii, string.Format(format, args)); - private static List> ReadTypes(BinaryReader tdbReader) + internal static void LoadEntities(string basePath) { - var constructorTypes = new[] { typeof(I) }; + IIndexInfo itemIndexInfo = new EntityTypeIndex("Items"); + IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); + IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - var count = tdbReader.ReadInt32(); + Mobiles = EntityPersistence.LoadIndex(basePath, mobileIndexInfo, out List> mobiles); + Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, out List> items); + Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, out List> guilds); - var types = new List>(count); - - for (var i = 0; i < count; ++i) - { - var typeName = tdbReader.ReadString(); - - var t = AssemblyHandler.FindTypeByFullName(typeName, false); - - if (t?.IsAbstract != false) - { - WriteConsoleLine("failed"); - - var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; - - WriteConsoleLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)"); - - if (Console.ReadKey(true).Key == ConsoleKey.Y) - { - types.Add(null); - WriteConsole("Loading..."); - continue; - } - - WriteConsoleLine("Types will not be deleted. An exception will be thrown."); - - throw new Exception($"Bad type '{typeName}'"); - } - - var ctor = t.GetConstructor(constructorTypes); - - if (ctor != null) - { - types.Add(new Tuple(ctor, typeName)); - } - else - { - throw new Exception($"Type '{t}' does not have a serialization constructor"); - } - } - - return types; - } - - private static Dictionary LoadIndex(IIndexInfo indexInfo, out List> entities) where T : class, ISerializable - { - var map = new Dictionary(); - object[] ctorArgs = new object[1]; - - var indexType = indexInfo.TypeName; - - string indexPath = Path.Combine(_savePath, indexType, $"{indexType}.idx"); - string typesPath = Path.Combine(_savePath, indexType, $"{indexType}.tdb"); - - entities = new List>(); - - if (!File.Exists(indexPath) || !File.Exists(typesPath)) - { - return map; - } - - using FileStream idx = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read); - BinaryReader idxReader = new BinaryReader(idx); - - using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read); - BinaryReader tdbReader = new BinaryReader(tdb); - - List> types = ReadTypes(tdbReader); - - var count = idxReader.ReadInt32(); - - for (int i = 0; i < count; ++i) - { - var typeID = idxReader.ReadInt32(); - var number = idxReader.ReadUInt32(); - var pos = idxReader.ReadInt64(); - var length = idxReader.ReadInt32(); - - Tuple objs = types[typeID]; - - if (objs == null) - { - continue; - } - - T t; - ConstructorInfo ctor = objs.Item1; - I indexer = indexInfo.CreateIndex(number); - - ctorArgs[0] = indexer; - t = ctor.Invoke(ctorArgs) as T; - - if (t != null) - { - entities.Add(new EntityIndex(t, typeID, pos, length)); - map[indexer] = t; - } - } - - tdbReader.Close(); - idxReader.Close(); - - return map; - } - - private static void LoadData(IIndexInfo indexInfo, List> entities) where T : class, ISerializable - { - var indexType = indexInfo.TypeName; - - string dataPath = Path.Combine(_savePath, indexType, $"{indexType}.bin"); - - if (!File.Exists(dataPath)) - { - return; - } - - using FileStream bin = new FileStream(dataPath, FileMode.Open, FileAccess.Read, FileShare.Read); - - BufferReader br = null; - - foreach (var entry in entities) - { - T t = entry.Entity; - - // Skip this entry - if (t == null) - { - bin.Seek(entry.Length, SeekOrigin.Current); - continue; - } - - var buffer = GC.AllocateUninitializedArray(entry.Length); - if (br == null) - { - br = new BufferReader(buffer); - } - else - { - br.SwapBuffers(buffer, out _); - } - - bin.Read(buffer.AsSpan()); - string error; - - try - { - t.Deserialize(br); - - error = br.Position != entry.Length - ? $"Serialized object was {entry.Length} bytes, but {br.Position} bytes deserialized" - : null; - } - catch (Exception e) - { - error = e.ToString(); - } - - if (error == null) - { - t.InitializeSaveBuffer(buffer); - } - else - { - Utility.PushColor(ConsoleColor.Red); - WriteConsoleLine($"***** Bad deserialize of {t.GetType()} *****"); - WriteConsoleLine(error); - Utility.PopColor(); - - WriteConsoleLine("Delete the object and continue? (y/n)"); - - if (Console.ReadKey(true).Key != ConsoleKey.Y) - { - throw new Exception("Deserialization failed."); - } - t.Delete(); - } - } + EntityPersistence.LoadData(basePath, mobileIndexInfo, mobiles); + EntityPersistence.LoadData(basePath, itemIndexInfo, items); + EntityPersistence.LoadData(basePath, guildIndexInfo, guilds); } public static void Load() @@ -416,18 +248,7 @@ namespace Server WriteConsole("Loading..."); var watch = Stopwatch.StartNew(); - IIndexInfo itemIndexInfo = new EntityTypeIndex("Items"); - IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); - IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - - Mobiles = LoadIndex(mobileIndexInfo, out List> mobiles); - Items = LoadIndex(itemIndexInfo, out List> items); - Guilds = LoadIndex(guildIndexInfo, out List> guilds); - - LoadData(mobileIndexInfo, mobiles); - LoadData(itemIndexInfo, items); - LoadData(guildIndexInfo, guilds); - + Persistence.Load(_savePath); EventSink.InvokeWorldLoad(); ProcessSafetyQueues(); @@ -510,26 +331,6 @@ namespace Server ProcessSafetyQueues(); } - private static void TraceException(Exception ex) - { - try - { - using var op = new StreamWriter("save-errors.log", true); - op.WriteLine("# {0}", DateTime.UtcNow); - - op.WriteLine(ex); - - op.WriteLine(); - op.WriteLine(); - } - catch - { - // ignored - } - - Console.WriteLine(ex); - } - private static void TraceSave(params IEnumerable>[] entityTypes) { try @@ -559,17 +360,22 @@ namespace Server } } - public static void WriteFiles(object state) + internal static void WriteEntities(string basePath) { IIndexInfo itemIndexInfo = new EntityTypeIndex("Items"); IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - Exception exception = null; + EntityPersistence.WriteEntities(mobileIndexInfo, Mobiles, MobileTypes, basePath, out var mobileCounts); + EntityPersistence.WriteEntities(itemIndexInfo, Items, ItemTypes, basePath, out var itemCounts); + EntityPersistence.WriteEntities(guildIndexInfo, Guilds, GuildTypes, basePath, out var guildCounts); - Dictionary mobileCounts = null; - Dictionary itemCounts = null; - Dictionary guildCounts = null; + TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList()); + } + + public static void WriteFiles(object state) + { + Exception exception = null; var tempPath = Path.Combine(_tempSavePath, Utility.GetTimeStamp()); @@ -578,9 +384,7 @@ namespace Server var watch = Stopwatch.StartNew(); WriteConsole("Writing snapshot..."); - WriteEntities(mobileIndexInfo, Mobiles, MobileTypes, tempPath, out mobileCounts); - WriteEntities(itemIndexInfo, Items, ItemTypes, tempPath, out itemCounts); - WriteEntities(guildIndexInfo, Guilds, GuildTypes, tempPath, out guildCounts); + Persistence.WriteSnapshot(tempPath); watch.Stop(); @@ -598,7 +402,7 @@ namespace Server Utility.PushColor(ConsoleColor.Red); Console.WriteLine("failed"); Utility.PopColor(); - TraceException(exception); + Persistence.TraceException(exception); BroadcastStaff(0x35, true, "Writing world save snapshot failed."); } @@ -606,14 +410,12 @@ namespace Server { try { - TraceSave(mobileCounts.ToList(), itemCounts.ToList(), guildCounts.ToList()); - EventSink.InvokeWorldSavePostSnapshot(_savePath, tempPath); Directory.Move(tempPath, _savePath); } catch (Exception ex) { - TraceException(ex); + Persistence.TraceException(ex); } } @@ -622,69 +424,6 @@ namespace Server Timer.DelayCall(FinishWorldSave); } - private static void WriteEntities( - IIndexInfo indexInfo, - Dictionary entities, - List types, - string savePath, - out Dictionary counts - ) where T : class, ISerializable - { - counts = new Dictionary(); - - var typeName = indexInfo.TypeName; - - var path = Path.Combine(savePath, typeName); - - AssemblyHandler.EnsureDirectory(path); - - string idxPath = Path.Combine(path, $"{typeName}.idx"); - string tdbPath = Path.Combine(path, $"{typeName}.tdb"); - string binPath = Path.Combine(path, $"{typeName}.bin"); - - using var idx = new BinaryFileWriter(idxPath, false); - using var tdb = new BinaryFileWriter(tdbPath, false); - using var bin = new BinaryFileWriter(binPath, true); - - idx.Write(entities.Count); - foreach (var e in entities.Values) - { - long start = bin.Position; - - idx.Write(e.TypeRef); - idx.Write(e.Serial); - idx.Write(start); - - e.SerializeTo(bin); - - idx.Write((int)(bin.Position - start)); - - var type = e.GetType().FullName; - if (type != null) - { - counts[type] = (counts.TryGetValue(type, out var count) ? count : 0) + 1; - } - } - - tdb.Write(types.Count); - for (int i = 0; i < types.Count; ++i) - { - tdb.Write(types[i].FullName); - } - } - - private static void SaveEntities(IEnumerable list, DateTime serializeStart) where T : class, ISerializable - { - Parallel.ForEach(list, t => { - if (t is Item item && item.CanDecay() && item.LastMoved + item.DecayTime <= serializeStart) - { - EnqueueForDecay(item); - } - - t.Serialize(); - }); - } - private static void ProcessDecay() { while (_decayQueue.TryDequeue(out var item)) @@ -697,6 +436,27 @@ namespace Server } } + private static DateTime _serializationStart; + + internal 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(); + } + public static void Save() { if (WorldState != WorldState.Running) @@ -722,10 +482,7 @@ namespace Server try { - SaveEntities(Items.Values, now); - SaveEntities(Mobiles.Values, now); - SaveEntities(Guilds.Values, now); - + Persistence.Serialize(); EventSink.InvokeWorldSave(); } catch (Exception ex) @@ -755,7 +512,7 @@ namespace Server Utility.PushColor(ConsoleColor.Red); Console.WriteLine("failed"); Utility.PopColor(); - TraceException(exception); + Persistence.TraceException(exception); BroadcastStaff(0x35, true, "World save failed."); } @@ -899,15 +656,6 @@ namespace Server [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void RemoveGuild(BaseGuild guild) => Guilds.Remove(guild.Serial); - private static void SerializeTo(this ISerializable entity, IGenericWriter writer) - { - var saveBuffer = entity.SaveBuffer; - writer.Write(saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position)); - - // Resize to exact buffer size - entity.SaveBuffer.Resize((int)entity.SaveBuffer.Position); - } - private static void WriteConsole(string message) { var now = DateTime.UtcNow; diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index 6498a5166..ee070b29c 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -15,17 +15,17 @@ namespace Server.Accounting public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0); public static readonly TimeSpan InactiveDuration = TimeSpan.FromDays(180.0); public static readonly TimeSpan EmptyInactiveDuration = TimeSpan.FromDays(30.0); - private readonly Mobile[] m_Mobiles; + private Mobile[] m_Mobiles; private AccessLevel m_AccessLevel; private List m_Comments; private PasswordProtectionAlgorithm m_PasswordAlgorithm; private List m_Tags; private TimeSpan m_TotalGameTime; - private Timer m_YoungTimer; + private BufferWriter _saveBuffer; - public Account(string username, string password) + public Account(string username, string password) : this(Accounts.NewAccount) { Username = username; @@ -44,8 +44,33 @@ namespace Server.Accounting Accounts.Add(this); } + public Account(Serial serial) + { + Serial = serial; + + var ourType = GetType(); + TypeRef = Accounts.Types.IndexOf(ourType); + + if (TypeRef == -1) + { + Accounts.Types.Add(ourType); + TypeRef = Accounts.Types.Count - 1; + } + } + public Account(XmlElement node) { + Serial = Accounts.NewAccount; + + var ourType = GetType(); + TypeRef = Accounts.Types.IndexOf(ourType); + + if (TypeRef == -1) + { + Accounts.Types.Add(ourType); + TypeRef = Accounts.Types.Count - 1; + } + Username = Utility.GetText(node["username"], "empty"); Enum.TryParse(Utility.GetText(node["passwordAlgorithm"], null), true, out m_PasswordAlgorithm); @@ -199,7 +224,7 @@ namespace Server.Accounting /// /// The date and time of when this account was created. /// - public DateTime Created { get; } + public DateTime Created { get; private set; } /// /// Gets or sets the date and time when this account was last accessed. @@ -245,6 +270,146 @@ namespace Server.Accounting } } + BufferWriter ISerializable.SaveBuffer + { + get => _saveBuffer; + set => _saveBuffer = value; + } + + public int TypeRef { get; private set; } + + public Serial Serial { get; set; } + + public void Deserialize(IGenericReader reader) + { + Username = reader.ReadString(); + m_PasswordAlgorithm = (PasswordProtectionAlgorithm)reader.ReadInt(); + Password = reader.ReadString(); + m_AccessLevel = (AccessLevel)reader.ReadInt(); + Flags = reader.ReadInt(); + Created = reader.ReadDateTime(); + LastLogin = reader.ReadDateTime(); + + TotalGold = reader.ReadInt(); + TotalPlat = reader.ReadInt(); + + m_Mobiles = new Mobile[7]; + var length = reader.ReadInt(); + for (int i = 0; i < length; i++) + { + m_Mobiles[i] = reader.ReadEntity(); + } + + length = reader.ReadInt(); + m_Comments = length > 0 ? new List(length) : null; + for (int i = 0; i < length; i++) + { + m_Comments!.Add(new AccountComment(reader)); + } + + length = reader.ReadInt(); + m_Tags = length > 0 ? new List(length) : null; + for (int i = 0; i < length; i++) + { + m_Tags!.Add(new AccountTag(reader)); + } + + length = reader.ReadInt(); + LoginIPs = new IPAddress[length]; + for (int i = 0; i < length; i++) + { + if (IPAddress.TryParse(reader.ReadString(), out var address)) + { + LoginIPs[i] = Utility.Intern(address); + } + } + + length = reader.ReadInt(); + IPRestrictions = new string[length]; + for (int i = 0; i < length; i++) + { + IPRestrictions[i] = reader.ReadString(); + } + + for (var i = 0; i < m_Mobiles.Length; ++i) + { + if (m_Mobiles[i] != null) + { + m_Mobiles[i].Account = this; + } + } + + var totalGameTime = reader.ReadTimeSpan(); + if (totalGameTime == TimeSpan.Zero) + { + for (var i = 0; i < m_Mobiles.Length; i++) + { + if (m_Mobiles[i] is PlayerMobile m) + { + totalGameTime += m.GameTime; + } + } + } + + m_TotalGameTime = totalGameTime; + + if (Young) + { + CheckYoung(); + } + } + + public void Serialize(IGenericWriter writer) + { + writer.Write(Username); + writer.Write((int)m_PasswordAlgorithm); + writer.Write(Password); + writer.Write((int)m_AccessLevel); + writer.Write(Flags); + writer.Write(Created); + writer.Write(LastLogin); + writer.Write(TotalGold); + writer.Write(TotalPlat); + + writer.Write(Count); + for (int i = 0; i < m_Mobiles.Length; i++) + { + var m = m_Mobiles[i]; + if (m != null) + { + writer.Write(m); + } + } + + var length = m_Comments?.Count ?? 0; + writer.Write(length); + for (int i = 0; i < length; i++) + { + m_Comments![i].Serialize(writer); + } + + length = m_Tags?.Count ?? 0; + writer.Write(length); + for (int i = 0; i < length; i++) + { + m_Tags![i].Serialize(writer); + } + + writer.Write(LoginIPs.Length); + for (int i = 0; i < LoginIPs.Length; i++) + { + writer.Write(LoginIPs[i].ToString()); + } + + writer.Write(IPRestrictions.Length); + for (int i = 0; i < IPRestrictions.Length; i++) + { + writer.Write(IPRestrictions[i]); + } + + writer.Write(TotalGameTime); + } + /// /// Deletes the account, all characters of the account, and all houses of those characters /// @@ -277,9 +442,12 @@ namespace Server.Accounting --AccountHandler.IPTable[LoginIPs[0]]; } - Accounts.Remove(Username); + Deleted = true; + Accounts.Remove(this); } + public bool Deleted { get; private set; } + /// /// Account username. Case insensitive validation. /// @@ -784,7 +952,7 @@ namespace Server.Accounting /// /// The XmlElement from which to deserialize. /// String list. Value will never be null. - public static string[] LoadAccessCheck(XmlElement node) + private static string[] LoadAccessCheck(XmlElement node) { string[] stringList; var accessCheck = node["accessCheck"]; @@ -818,7 +986,7 @@ namespace Server.Accounting /// /// The XmlElement from which to deserialize. /// Address list. Value will never be null. - public static IPAddress[] LoadAddressList(XmlElement node) + private static IPAddress[] LoadAddressList(XmlElement node) { IPAddress[] list; var addressList = node["addressList"]; @@ -867,7 +1035,7 @@ namespace Server.Accounting /// /// The XmlElement instance from which to deserialize. /// Mobile list. Value will never be null. - public static Mobile[] LoadMobiles(XmlElement node) + private static Mobile[] LoadMobiles(XmlElement node) { var list = new Mobile[7]; var chars = node["chars"]; @@ -905,7 +1073,7 @@ namespace Server.Accounting /// /// The XmlElement from which to deserialize. /// Comment list. Value will never be null. - public static List LoadComments(XmlElement node) + private static List LoadComments(XmlElement node) { List list = null; var comments = node["comments"]; @@ -935,7 +1103,7 @@ namespace Server.Accounting /// /// The XmlElement from which to deserialize. /// Tag list. Value will never be null. - public static List LoadTags(XmlElement node) + private static List LoadTags(XmlElement node) { List list = null; var tags = node["tags"]; diff --git a/Projects/UOContent/Accounting/AccountComment.cs b/Projects/UOContent/Accounting/AccountComment.cs index ee2bdeb64..b966490a2 100644 --- a/Projects/UOContent/Accounting/AccountComment.cs +++ b/Projects/UOContent/Accounting/AccountComment.cs @@ -30,6 +30,17 @@ namespace Server.Accounting m_Content = Utility.GetText(node, ""); } + /// + /// Deserializes an AccountComment instance. + /// + /// The deserialization reader + public AccountComment(IGenericReader reader) + { + AddedBy = reader.ReadString(); + LastModified = reader.ReadDateTime(); + m_Content = reader.ReadString(); + } + /// /// A string representing who added this comment. /// @@ -69,5 +80,16 @@ namespace Server.Accounting xml.WriteEndElement(); } + + /// + /// Serializes this AccountComment instance. + /// + /// The serialization writer. + public void Serialize(IGenericWriter writer) + { + writer.Write(AddedBy ?? "empty"); + writer.Write(LastModified); + writer.Write(m_Content); + } } } diff --git a/Projects/UOContent/Accounting/AccountTag.cs b/Projects/UOContent/Accounting/AccountTag.cs index ae6b85673..b7d5379cb 100644 --- a/Projects/UOContent/Accounting/AccountTag.cs +++ b/Projects/UOContent/Accounting/AccountTag.cs @@ -25,6 +25,16 @@ namespace Server.Accounting Value = Utility.GetText(node, ""); } + /// + /// Deserializes an AccountTag instance . + /// + /// The deserialization reader + public AccountTag(IGenericReader reader) + { + Name = reader.ReadString(); + Value = reader.ReadString(); + } + /// /// Gets or sets the name of this tag. /// @@ -46,5 +56,15 @@ namespace Server.Accounting xml.WriteString(Value); xml.WriteEndElement(); } + + /// + /// Serializes this AccountTag instance to an XmlTextWriter. + /// + /// The serialization writer. + public void Serialize(IGenericWriter writer) + { + writer.Write(Name ?? "empty"); + writer.Write(Value); + } } } diff --git a/Projects/UOContent/Accounting/Accounts.cs b/Projects/UOContent/Accounting/Accounts.cs index f75c1fd58..0425b644c 100644 --- a/Projects/UOContent/Accounting/Accounts.cs +++ b/Projects/UOContent/Accounting/Accounts.cs @@ -7,50 +7,92 @@ namespace Server.Accounting { public static class Accounts { - private static Dictionary m_Accounts = new(); + private static readonly Dictionary _accountsByName = new(32, StringComparer.OrdinalIgnoreCase); + private static Dictionary _accountsById = new(32); + private static Serial _lastAccount; + internal static List Types { get; } = new(); - static Accounts() + private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); + + public static Serial NewAccount { + get + { + uint 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; + } } - public static int Count => m_Accounts.Count; + public static int Count => _accountsByName.Count; - public static void Configure() + public static void Configure() => + Persistence.Register(Serialize, WriteSnapshot, Deserialize); + + internal static void Serialize() => + EntityPersistence.SaveEntities(_accountsById.Values, account => account.Serialize()); + + internal static void WriteSnapshot(string basePath) { - EventSink.WorldLoad += Load; - EventSink.WorldSave += Save; + IIndexInfo indexInfo = new EntityTypeIndex("Accounts"); + EntityPersistence.WriteEntities(indexInfo, _accountsById, Types, basePath, out _); } - public static IEnumerable GetAccounts() => m_Accounts.Values; + public static IEnumerable GetAccounts() => _accountsByName.Values; public static IAccount GetAccount(string username) { - m_Accounts.TryGetValue(username, out var a); - + _accountsByName.TryGetValue(username, out var a); return a; } public static void Add(IAccount a) { - m_Accounts[a.Username] = a; + _accountsByName[a.Username] = a; + _accountsById[a.Serial] = a; } - public static void Remove(string username) + public static void Remove(IAccount a) { - m_Accounts.Remove(username); + _accountsByName.Remove(a.Username); + _accountsById.Remove(a.Serial); } - public static void Load() + internal static void Deserialize(string path) { - m_Accounts = new Dictionary(32, StringComparer.OrdinalIgnoreCase); + var filePath = Path.Combine(path, "Accounts", "accounts.xml"); - var filePath = Path.Combine("Saves/Accounts", "accounts.xml"); - - if (!File.Exists(filePath)) + // Backward Compatibility + if (File.Exists(filePath)) { + DeserializeXml(filePath); return; } + IIndexInfo indexInfo = new EntityTypeIndex("Accounts"); + + _accountsById = EntityPersistence.LoadIndex(path, indexInfo, out List> accounts); + EntityPersistence.LoadData(path, indexInfo, accounts); + + foreach (var a in _accountsById.Values) + { + _accountsByName[a.Username] = a; + } + } + + private static void DeserializeXml(string filePath) + { var doc = new XmlDocument(); doc.Load(filePath); @@ -74,29 +116,10 @@ namespace Server.Accounting } } - public static void Save() + public static IAccount FindAccount(Serial serial) { - AssemblyHandler.EnsureDirectory("Saves/Accounts"); - - var filePath = Path.Combine("Saves/Accounts", "accounts.xml"); - - using var op = new StreamWriter(filePath); - var xml = new XmlTextWriter(op) { Formatting = Formatting.Indented, IndentChar = '\t', Indentation = 1 }; - - xml.WriteStartDocument(true); - - xml.WriteStartElement("accounts"); - - xml.WriteAttributeString("count", m_Accounts.Count.ToString()); - - foreach (Account a in GetAccounts()) - { - a.Save(xml); - } - - xml.WriteEndElement(); - - xml.Close(); + _accountsById.TryGetValue(serial, out var account); + return account; } } }