diff --git a/Projects/Server/AssemblyHandler.cs b/Projects/Server/AssemblyHandler.cs index fcb2866a8..9ba3da8eb 100644 --- a/Projects/Server/AssemblyHandler.cs +++ b/Projects/Server/AssemblyHandler.cs @@ -14,12 +14,14 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; +using Server.Logging; namespace Server; @@ -27,6 +29,7 @@ public static class AssemblyHandler { private static readonly Dictionary m_TypeCaches = new(); private static TypeCache m_NullCache; + public static Assembly[] Assemblies { get; set; } internal static Assembly AssemblyResolver(object sender, ResolveEventArgs args) @@ -66,6 +69,8 @@ public static class AssemblyHandler EnsureAssemblyDirectories(); var assemblyDirectories = ServerConfiguration.AssemblyDirectories; + Assembly assembly = null; + foreach (var assemblyDir in assemblyDirectories) { var assemblyPath = PathUtility.GetFullPath(Path.Combine(assemblyDir, fileName), Core.BaseDirectory); @@ -74,12 +79,17 @@ public static class AssemblyHandler var assemblyNameCheck = AssemblyName.GetAssemblyName(assemblyPath); if (assemblyNameCheck.FullName == fullName) { - return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); + assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); + break; } } } - return null; + // This forces the type caching to be generated. + // We need this for world loading to find types by hash. + GetTypeCache(assembly); + + return assembly; } public static Assembly LoadAssemblyByFileName(string assemblyFile) @@ -154,6 +164,11 @@ public static class AssemblyHandler } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetTypeHash(Type type) => GetTypeHash(type.FullName); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetTypeHash(string key) => key == null ? 0 : HashUtility.ComputeHash64(key); public static TypeCache GetTypeCache(Assembly asm) { @@ -195,84 +210,120 @@ public static class AssemblyHandler return null; } + + public static Type FindTypeByHash(ulong hash) + { + for (var i = 0; i < Assemblies.Length; i++) + { + foreach (var type in GetTypeCache(Assemblies[i]).GetTypesByHash(hash, true, false)) + { + return type; + } + } + + foreach(var type in GetTypeCache(Core.Assembly).GetTypesByHash(hash, true, false)) + { + return type; + } + + return null; + } } public class TypeCache { - private readonly Dictionary _nameMap = new(); - private readonly Dictionary _nameMapInsensitive = new(); - private readonly Dictionary _fullNameMap = new(); - private readonly Dictionary _fullNameMapInsensitive = new(); + private static ILogger logger = LogFactory.GetLogger(typeof(TypeCache)); + + private Dictionary _nameMap = new(); + private Dictionary _nameMapInsensitive = new(); + private Dictionary _fullNameMap = new(); + private Dictionary _fullNameMapInsensitive = new(); public TypeCache(Assembly asm) { Types = asm?.GetTypes() ?? Type.EmptyTypes; - var nameMap = new Dictionary>(); - var nameMapInsensitive = new Dictionary>(); - var fullNameMap = new Dictionary>(); - var fullNameMapInsensitive = new Dictionary>(); + var nameMap = new Dictionary>(); + var nameMapInsensitive = new Dictionary>(); + var fullNameMap = new Dictionary>(); + var fullNameMapInsensitive = new Dictionary>(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - void addTypeToRefs(int index, string fullTypeName) + void addTypeToRefs(Type type, string typeName, string fullTypeName) { - var typeName = fullTypeName[(fullTypeName.LastIndexOf('.') + 1)..]; - AddToRefs(index, typeName, nameMap); - AddToRefs(index, typeName.ToLower(), nameMapInsensitive); - AddToRefs(index, fullTypeName, fullNameMap); - AddToRefs(index, fullTypeName.ToLower(), fullNameMapInsensitive); + AddToRefs(type, typeName, nameMap); + AddToRefs(type, typeName.ToLower(), nameMapInsensitive); + AddToRefs(type, fullTypeName, fullNameMap); + AddToRefs(type, fullTypeName.ToLower(), fullNameMapInsensitive); } var aliasType = typeof(TypeAliasAttribute); for (var i = 0; i < Types.Length; i++) { var current = Types[i]; - addTypeToRefs(i, current.FullName); + addTypeToRefs(current, current.Name, current.FullName ?? ""); if (current.GetCustomAttribute(aliasType, false) is TypeAliasAttribute alias) { for (var j = 0; j < alias.Aliases.Length; j++) { - addTypeToRefs(i, alias.Aliases[j]); + var fullTypeName = alias.Aliases[j]; + var typeName = fullTypeName[(fullTypeName.LastIndexOf('.')+1)..]; + addTypeToRefs(current, typeName, fullTypeName); } } } foreach (var (key, value) in nameMap) { - _nameMap[key] = value.ToArray(); + _nameMap[HashUtility.ComputeHash64(key)] = value.ToArray(); } foreach (var (key, value) in nameMapInsensitive) { - _nameMapInsensitive[key] = value.ToArray(); + _nameMapInsensitive[HashUtility.ComputeHash64(key)] = value.ToArray(); } foreach (var (key, value) in fullNameMap) { - _fullNameMap[key] = value.ToArray(); + var values = value.ToArray(); + _fullNameMap[HashUtility.ComputeHash64(key)] = value.ToArray(); +#if DEBUG + if (values.Length > 1) + { + for (var i = 0; i < values.Length; i++) + { + var type = values[i]; + logger.Warning( + "Duplicate type {Type1} for {Name}.", + type, + key + ); + } + } +#endif } foreach (var (key, value) in fullNameMapInsensitive) { - _fullNameMapInsensitive[key] = value.ToArray(); + _fullNameMapInsensitive[HashUtility.ComputeHash64(key)] = value.ToArray(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddToRefs(int index, string key, Dictionary> map) + private static void AddToRefs(Type type, string key, Dictionary> map) { - if (key == null) + if (string.IsNullOrEmpty(key)) { return; } if (map.TryGetValue(key, out var refs)) { - refs.Add(index); + refs.Add(type); } else { - refs = new HashSet { index }; + refs = new HashSet { type }; map.Add(key, refs); } } @@ -280,64 +331,50 @@ public class TypeCache public Type[] Types { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TypeEnumerable GetTypesByName(string name, bool full, bool ignoreCase) => new(name, this, full, ignoreCase); + public TypeEnumerator GetTypesByName(string name, bool full, bool ignoreCase) => new(name, this, full, ignoreCase); - public ref struct TypeEnumerable - { - private readonly TypeCache _cache; - private readonly string _name; - private readonly bool _ignoreCase; - private readonly bool _full; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TypeEnumerable(string name, TypeCache cache, bool full, bool ignoreCase) - { - _name = name; - _cache = cache; - _ignoreCase = ignoreCase; - _full = full; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TypeEnumerator GetEnumerator() => new(_name, _cache, _full, _ignoreCase); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TypeEnumerator GetTypesByHash(ulong hash, bool full, bool ignoreCase) => new(hash, this, full, ignoreCase); public ref struct TypeEnumerator { - private readonly TypeCache _cache; - private readonly int[] _values; + private Type[] _values; private int _index; private Type _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal TypeEnumerator(string name, TypeCache cache, bool full, bool ignoreCase) + : this(HashUtility.ComputeHash64(ignoreCase ? name.ToLower() : name), cache, full, ignoreCase) { - _cache = cache; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal TypeEnumerator(ulong hash, TypeCache cache, bool full, bool ignoreCase) + { if (ignoreCase) { - var map = full ? _cache._fullNameMapInsensitive : _cache._nameMapInsensitive; - _values = map.TryGetValue(name.ToLower(), out var values) ? values : Array.Empty(); + var map = full ? cache._fullNameMapInsensitive : cache._nameMapInsensitive; + _values = map.TryGetValue(hash, out var values) ? values : Array.Empty(); } else { - var map = full ? _cache._fullNameMap : _cache._nameMap; - _values = map.TryGetValue(name, out var values) ? values : Array.Empty(); + var map = full ? cache._fullNameMap : cache._nameMap; + _values = map.TryGetValue(hash, out var values) ? values : Array.Empty(); } _index = 0; _current = default; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TypeEnumerator GetEnumerator() => this; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - int[] localList = _values; - - if ((uint)_index < (uint)localList.Length) + if ((uint)_index < (uint)_values.Length) { - _current = _cache.Types[_values[_index++]]; - + _current = _values[_index++]; return true; } diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs index b006864d4..2e164b918 100644 --- a/Projects/Server/Guild.cs +++ b/Projects/Server/Guild.cs @@ -31,25 +31,15 @@ public abstract class BaseGuild : ISerializable { Serial = World.NewGuild; World.AddGuild(this); - - SetTypeRef(GetType()); } protected BaseGuild(Serial serial) { Serial = serial; - SetTypeRef(GetType()); } public void SetTypeRef(Type type) { - TypeRef = World.GuildTypes.IndexOf(type); - - if (TypeRef == -1) - { - World.GuildTypes.Add(type); - TypeRef = World.GuildTypes.Count - 1; - } } public abstract string Abbreviation { get; set; } diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs index 0d1614332..b0335c555 100644 --- a/Projects/Server/IEntity.cs +++ b/Projects/Server/IEntity.cs @@ -43,10 +43,6 @@ public class Entity : IEntity public Entity(Serial serial) => Serial = serial; - public void SetTypeRef(Type type) - { - } - DateTime ISerializable.Created { get; set; } = Core.Now; DateTime ISerializable.LastSerialized { get; set; } = DateTime.MaxValue; diff --git a/Projects/Server/Items/Container.cs b/Projects/Server/Items/Container.cs index e1eeb8cab..f9d3fca46 100644 --- a/Projects/Server/Items/Container.cs +++ b/Projects/Server/Items/Container.cs @@ -1809,8 +1809,7 @@ public class Container : Item public class ContainerData { - private static ILogger _logger; - private static ILogger Logger => _logger ??= LogFactory.GetLogger(typeof(ContainerData)); + private static ILogger logger = LogFactory.GetLogger(typeof(ContainerData)); private static readonly Dictionary m_Table; static ContainerData() @@ -1875,7 +1874,7 @@ public class ContainerData if (m_Table.ContainsKey(id)) { - Logger.Warning("double ItemID entry in Data\\containers.cfg"); + logger.Warning("double ItemID entry in Data\\containers.cfg"); } else { diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index a85317de4..66f78ecf2 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -217,24 +217,15 @@ public class Item : IHued, IComparable, ISpawnable, IObjectPropertyListEnt SetLastMoved(); World.AddEntity(this); - SetTypeRef(GetType()); } public Item(Serial serial) { Serial = serial; - SetTypeRef(GetType()); } public void SetTypeRef(Type type) { - TypeRef = World.ItemTypes.IndexOf(type); - - if (TypeRef == -1) - { - World.ItemTypes.Add(type); - TypeRef = World.ItemTypes.Count - 1; - } } public int TempFlags @@ -786,8 +777,6 @@ public class Item : IHued, IComparable, ISpawnable, IObjectPropertyListEnt [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } - public int TypeRef { get; private set; } - public virtual void Serialize(IGenericWriter writer) { writer.Write(9); // version diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 6425b9e4f..fd1799d98 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -340,7 +340,6 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis DefaultMobileInit(); World.AddEntity(this); - SetTypeRef(GetType()); } public Mobile(Serial serial) @@ -351,19 +350,10 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis Aggressed = new List(); NextSkillTime = Core.TickCount; DamageEntries = new List(); - - SetTypeRef(GetType()); } public void SetTypeRef(Type type) { - TypeRef = World.MobileTypes.IndexOf(type); - - if (TypeRef == -1) - { - World.MobileTypes.Add(type); - TypeRef = World.MobileTypes.Count - 1; - } } public static bool DragEffects { get; set; } = true; @@ -2272,8 +2262,6 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } - public int TypeRef { get; private set; } - public virtual void Serialize(IGenericWriter writer) { writer.Write(33); // version diff --git a/Projects/Server/Serialization/AdhocPersistence.cs b/Projects/Server/Serialization/AdhocPersistence.cs index 086179409..8d390dc86 100644 --- a/Projects/Server/Serialization/AdhocPersistence.cs +++ b/Projects/Server/Serialization/AdhocPersistence.cs @@ -14,6 +14,8 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; using System.Threading.Tasks; @@ -27,30 +29,53 @@ public static class AdhocPersistence * Note: The buffer may not be the same after returning from the function if more data is written * than the initial buffer can handle. */ - public static BufferWriter Serialize(Action serializer) + public static BufferWriter Serialize(Action serializer, ConcurrentQueue types) { - var saveBuffer = new BufferWriter(true); + var saveBuffer = new BufferWriter(true, types); serializer(saveBuffer); return saveBuffer; } /** * Writes a buffer to disk. This function should be called asynchronously. + * Writes the filePath for the binary data, and an accompanying SerializedTypes.db file of all possible types. */ - public static void WriteSnapshot(string filePath, Span buffer) + public static void WriteSnapshot(FileInfo file, Span buffer) { - var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); - var file = new FileInfo(fullPath); - PathUtility.EnsureDirectory(file.DirectoryName); + var dirPath = file.DirectoryName; + PathUtility.EnsureDirectory(dirPath); - using var fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write); + using var fs = new FileStream(file.FullName, FileMode.Create, FileAccess.Write); fs.Write(buffer); } - public static void SerializeAndSnapshot(string filePath, Action serializer) + /** + * Serializes to a memory buffer synchronously, then flushes to the path asynchronously. + * See WriteSnapshot for more info about how the snapshot. + */ + public static void SerializeAndSnapshot(string filePath, Action serializer, ConcurrentQueue types = null) { - var saveBuffer = Serialize(serializer); - Task.Run(() => { WriteSnapshot(filePath, saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position)); }); + types ??= new ConcurrentQueue(); + var saveBuffer = Serialize(serializer, types); + Task.Run( + () => + { + var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); + var file = new FileInfo(fullPath); + + WriteSnapshot(file, saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position)); + + // TODO: Create a PooledHashSet if performance becomes an issue. + var typesSet = new HashSet(); + + // Dedupe the queue. + foreach (var type in types) + { + typesSet.Add(type); + } + + Persistence.WriteSerializedTypesSnapshot(file.DirectoryName, typesSet); + }); } public static void Deserialize(string filePath, Action deserializer) diff --git a/Projects/Server/Serialization/BinaryFileReader.cs b/Projects/Server/Serialization/BinaryFileReader.cs index eab5c7ffa..3cc5868a8 100644 --- a/Projects/Server/Serialization/BinaryFileReader.cs +++ b/Projects/Server/Serialization/BinaryFileReader.cs @@ -14,27 +14,34 @@ *************************************************************************/ using System; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Text; using Server.Buffers; using Server.Collections; +using Server.Logging; using Server.Text; namespace Server; public class BinaryFileReader : IGenericReader, IDisposable { + private static readonly ILogger logger = LogFactory.GetLogger(typeof(BinaryFileReader)); + + private Dictionary _typesDb; private BinaryReader _reader; private Encoding _encoding; - public BinaryFileReader(BinaryReader br, Encoding encoding = null) + public BinaryFileReader(BinaryReader br, Dictionary typesDb = null, Encoding encoding = null) { _reader = br; _encoding = encoding ?? TextEncoding.UTF8; + _typesDb = typesDb; } - public BinaryFileReader(Stream stream, Encoding encoding = null) : this(new BinaryReader(stream), encoding) + public BinaryFileReader(Stream stream, Dictionary typesDb = null, Encoding encoding = null) + : this(new BinaryReader(stream), typesDb, encoding) { } @@ -46,23 +53,20 @@ public class BinaryFileReader : IGenericReader, IDisposable public DateTime LastSerialized { get; init; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadString(bool intern = false) - { - if (!ReadBool()) - { - return null; - } + public string ReadString(bool intern = false) => ReadBool() ? ReadStringRaw(intern) : null; + public string ReadStringRaw(bool intern = false) + { var length = ((IGenericReader)this).ReadEncodedInt(); if (length <= 0) { - return intern ? Utility.Intern("") : ""; + return "".Intern(); } byte[] buffer = STArrayPool.Shared.Rent(length); var str = TextEncoding.GetString(buffer.AsSpan(0, length), _encoding); STArrayPool.Shared.Return(buffer); - return intern ? Utility.Intern(str) : str; + return intern ? str.Intern() : str; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -101,6 +105,62 @@ public class BinaryFileReader : IGenericReader, IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining)] public Serial ReadSerial() => (Serial)_reader.ReadUInt32(); + public Type ReadType() => + ReadByte() switch + { + 0 => null, + 1 => AssemblyHandler.FindTypeByFullName(ReadStringRaw()), // Backward compatibility + 2 => ReadTypeByHash() + }; + + public Type ReadTypeByHash() + { + var hash = ReadULong(); + var t = AssemblyHandler.FindTypeByHash(hash); + + if (t != null) + { + return t; + } + + if (_typesDb == null) + { + logger.Error( + new Exception($"The file SerializedTypes.db was not loaded. Type hash '{hash}' could not be found."), + "Invalid {Hash} at position {Position}", + hash, + Position + ); + + return null; + } + + if (!_typesDb.TryGetValue(hash, out var typeName)) + { + logger.Error( + new Exception($"Type hash '{hash}' is not present in the serialized types database."), + "Invalid type hash {Hash} at position {Position}", + hash, + Position + ); + + return null; + } + + t = AssemblyHandler.FindTypeByFullName(typeName, false); + + if (t == null) + { + logger.Error( + new Exception($"Type '{typeName}' was not found."), + "Type {Type} was not found.", + typeName + ); + } + + return t; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Read(Span buffer) => _reader.Read(buffer); diff --git a/Projects/Server/Serialization/BinaryFileWriter.cs b/Projects/Server/Serialization/BinaryFileWriter.cs index ceb7447c1..3febd65c1 100644 --- a/Projects/Server/Serialization/BinaryFileWriter.cs +++ b/Projects/Server/Serialization/BinaryFileWriter.cs @@ -14,6 +14,7 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.IO; namespace Server; @@ -23,11 +24,11 @@ public class BinaryFileWriter : BufferWriter, IDisposable private readonly Stream _file; private long _position; - public BinaryFileWriter(string filename, bool prefixStr) : - this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), prefixStr) + public BinaryFileWriter(string filename, bool prefixStr, ConcurrentQueue types = null) : + this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), prefixStr, types) {} - public BinaryFileWriter(Stream stream, bool prefixStr) : base(prefixStr) + public BinaryFileWriter(Stream stream, bool prefixStr, ConcurrentQueue types = null) : base(prefixStr, types) { _file = stream; _position = _file.Position; diff --git a/Projects/Server/Serialization/BufferReader.cs b/Projects/Server/Serialization/BufferReader.cs index 1a1f151af..eece3f03a 100644 --- a/Projects/Server/Serialization/BufferReader.cs +++ b/Projects/Server/Serialization/BufferReader.cs @@ -15,30 +15,40 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; using Server.Collections; +using Server.Logging; using Server.Text; namespace Server; public class BufferReader : IGenericReader { + private static readonly ILogger logger = LogFactory.GetLogger(typeof(BufferReader)); + + private Dictionary _typesDb; private Encoding _encoding; private byte[] _buffer; private int _position; public long Position => _position; - public BufferReader(byte[] buffer, Encoding encoding = null) + public BufferReader(byte[] buffer, Dictionary typesDb = null, Encoding encoding = null) { _buffer = buffer; _encoding = encoding ?? TextEncoding.UTF8; + _typesDb = typesDb; } - public BufferReader(byte[] buffer, DateTime lastSerialized) : this(buffer) => LastSerialized = lastSerialized; + public BufferReader(byte[] buffer, DateTime lastSerialized, Dictionary typesDb = null) : this(buffer) + { + LastSerialized = lastSerialized; + _typesDb = typesDb; + } public void Reset(byte[] newBuffer, out byte[] oldBuffer) { @@ -49,22 +59,20 @@ public class BufferReader : IGenericReader public DateTime LastSerialized { get; init; } - public string ReadString(bool intern = false) - { - if (!ReadBool()) - { - return null; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(bool intern = false) => ReadBool() ? ReadStringRaw(intern) : null; + public string ReadStringRaw(bool intern = false) + { var length = ((IGenericReader)this).ReadEncodedInt(); if (length <= 0) { - return intern ? Utility.Intern("") : ""; + return "".Intern(); } var str = TextEncoding.GetString(_buffer.AsSpan(_position, length), _encoding); _position += length; - return intern ? Utility.Intern(str) : str; + return intern ? str.Intern() : str; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -143,6 +151,62 @@ public class BufferReader : IGenericReader [MethodImpl(MethodImplOptions.AggressiveInlining)] public Serial ReadSerial() => (Serial)ReadUInt(); + public Type ReadType() => + ReadByte() switch + { + 0 => null, + 1 => AssemblyHandler.FindTypeByFullName(ReadStringRaw()), // Backward compatibility + 2 => ReadTypeByHash() + }; + + public Type ReadTypeByHash() + { + var hash = ReadULong(); + var t = AssemblyHandler.FindTypeByHash(hash); + + if (t != null) + { + return t; + } + + if (_typesDb == null) + { + logger.Error( + new Exception($"The file SerializedTypes.db was not loaded. Type hash '{hash}' could not be found."), + "Invalid {Hash} at position {Position}", + hash, + Position + ); + + return null; + } + + if (!_typesDb.TryGetValue(hash, out var typeName)) + { + logger.Error( + new Exception($"Type hash '{hash}' is not present in the serialized types database."), + "Invalid type hash {Hash} at position {Position}", + hash, + Position + ); + + return null; + } + + t = AssemblyHandler.FindTypeByFullName(typeName, false); + + if (t == null) + { + logger.Error( + new Exception($"Type '{typeName}' was not found."), + "Type {Type} was not found.", + typeName + ); + } + + return t; + } + public int Read(Span buffer) { var length = buffer.Length; diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index 325b33717..a3090c564 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -2,7 +2,7 @@ * ModernUO * * Copyright 2019-2022 - ModernUO Development Team * * Email: hi@modernuo.com * - * File: BufferedFileWriter.cs * + * File: BufferWriter.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 * @@ -14,6 +14,7 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; @@ -25,8 +26,9 @@ namespace Server; public class BufferWriter : IGenericWriter { - private readonly Encoding m_Encoding; - private readonly bool m_PrefixStrings; + private ConcurrentQueue _types; + private Encoding _encoding; + private bool _prefixStrings; private long _bytesWritten; private long _index; @@ -53,22 +55,24 @@ public class BufferWriter : IGenericWriter private byte[] _buffer; - public BufferWriter(byte[] buffer, bool prefixStr) + public BufferWriter(byte[] buffer, bool prefixStr, ConcurrentQueue types = null) { - m_PrefixStrings = prefixStr; - m_Encoding = TextEncoding.UTF8; + _prefixStrings = prefixStr; + _encoding = TextEncoding.UTF8; _buffer = buffer; + _types = types; } - public BufferWriter(bool prefixStr) : this(0, prefixStr) + public BufferWriter(bool prefixStr, ConcurrentQueue types = null) : this(0, prefixStr, types) { } - public BufferWriter(int count, bool prefixStr) + public BufferWriter(int count, bool prefixStr, ConcurrentQueue types = null) { - m_PrefixStrings = prefixStr; - m_Encoding = TextEncoding.UTF8; + _prefixStrings = prefixStr; + _encoding = TextEncoding.UTF8; _buffer = GC.AllocateUninitializedArray(count < 1 ? BufferSize : count); + _types = types; } public virtual long Position => Index; @@ -135,6 +139,7 @@ public class BufferWriter : IGenericWriter } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(BitArray bitArray) { var byteLength = BitArray.GetByteArrayLengthFromBitLength(bitArray.Length); @@ -145,6 +150,7 @@ public class BufferWriter : IGenericWriter Index += byteLength; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual long Seek(long offset, SeekOrigin origin) { Debug.Assert( @@ -168,9 +174,10 @@ public class BufferWriter : IGenericWriter }); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(string value) { - if (m_PrefixStrings) + if (_prefixStrings) { if (value == null) { @@ -188,6 +195,7 @@ public class BufferWriter : IGenericWriter } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(long value) { FlushIfNeeded(8); @@ -202,6 +210,7 @@ public class BufferWriter : IGenericWriter _buffer[Index++] = (byte)(value >> 56); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ulong value) { FlushIfNeeded(8); @@ -216,6 +225,7 @@ public class BufferWriter : IGenericWriter _buffer[Index++] = (byte)(value >> 56); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(int value) { FlushIfNeeded(4); @@ -226,6 +236,7 @@ public class BufferWriter : IGenericWriter _buffer[Index++] = (byte)(value >> 24); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(uint value) { FlushIfNeeded(4); @@ -236,6 +247,7 @@ public class BufferWriter : IGenericWriter _buffer[Index++] = (byte)(value >> 24); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(short value) { FlushIfNeeded(2); @@ -244,6 +256,7 @@ public class BufferWriter : IGenericWriter _buffer[Index++] = (byte)(value >> 8); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ushort value) { FlushIfNeeded(2); @@ -252,6 +265,7 @@ public class BufferWriter : IGenericWriter _buffer[Index++] = (byte)(value >> 8); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void Write(double value) { FlushIfNeeded(8); @@ -264,6 +278,7 @@ public class BufferWriter : IGenericWriter Index += 8; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void Write(float value) { FlushIfNeeded(4); @@ -300,9 +315,25 @@ public class BufferWriter : IGenericWriter [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Serial serial) => Write(serial.Value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(Type type) + { + if (type == null) + { + Write((byte)0); + } + else + { + Write((byte)0x2); // xxHash3 64bit + Write(AssemblyHandler.GetTypeHash(type)); + _types?.Enqueue(type); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void InternalWriteString(string value) { - var remaining = m_Encoding.GetByteCount(value); + var remaining = _encoding.GetByteCount(value); ((IGenericWriter)this).WriteEncodedInt(remaining); @@ -313,14 +344,14 @@ public class BufferWriter : IGenericWriter // It is much faster to encode to stack buffer, then copy to the real buffer Span span = stackalloc byte[Math.Min(BufferSize, 256)]; - var maxChars = span.Length / m_Encoding.GetMaxByteCount(1); + var maxChars = span.Length / _encoding.GetMaxByteCount(1); var charsLeft = value.Length; var current = 0; while (charsLeft > 0) { var charCount = Math.Min(charsLeft, maxChars); - var bytesWritten = m_Encoding.GetBytes(value.AsSpan(current, charCount), span); + var bytesWritten = _encoding.GetBytes(value.AsSpan(current, charCount), span); remaining -= bytesWritten; charsLeft -= charCount; current += charCount; diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index 355f21433..fa47736cd 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -14,6 +14,8 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; namespace Server; @@ -31,22 +33,22 @@ public static class GenericPersistence void Serialize() { - saveBuffer ??= new BufferWriter(true); + saveBuffer ??= new BufferWriter(true, World.SerializedTypes); saveBuffer.Seek(0, SeekOrigin.Begin); serializer(saveBuffer); } - void WriterSnapshot(string savePath) + void WriteSnapshot(string savePath) { string binPath = Path.Combine(savePath, name, $"{name}.bin"); var buffer = saveBuffer!.Buffer.AsSpan(0, (int)saveBuffer.Position); - AdhocPersistence.WriteSnapshot(binPath, buffer); + AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer); } - void Deserialize(string savePath) => + void Deserialize(string savePath, Dictionary typesDb) => AdhocPersistence.Deserialize(Path.Combine(savePath, name, $"{name}.bin"), deserializer); - Persistence.Register(name, Serialize, WriterSnapshot, Deserialize, priority); + Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, priority); } } diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs index 837c6e212..f5b29f611 100644 --- a/Projects/Server/Serialization/IGenericReader.cs +++ b/Projects/Server/Serialization/IGenericReader.cs @@ -38,6 +38,7 @@ public interface IGenericReader sbyte ReadSByte(); bool ReadBool(); Serial ReadSerial(); + Type ReadType(); DateTime ReadDateTime() => new(ReadLong(), DateTimeKind.Utc); TimeSpan ReadTimeSpan() => new(ReadLong()); diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs index 97aa76ca5..a7dd75a1a 100644 --- a/Projects/Server/Serialization/IGenericWriter.cs +++ b/Projects/Server/Serialization/IGenericWriter.cs @@ -37,6 +37,7 @@ public interface IGenericWriter void Write(sbyte value); void Write(bool value); void Write(Serial serial); + void Write(Type type); void Write(DateTime value) { diff --git a/Projects/Server/Serialization/ISerializable.cs b/Projects/Server/Serialization/ISerializable.cs index f49bd50f5..9452f87d3 100644 --- a/Projects/Server/Serialization/ISerializable.cs +++ b/Projects/Server/Serialization/ISerializable.cs @@ -14,6 +14,7 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.IO; namespace Server; @@ -28,7 +29,6 @@ public interface ISerializable long SavePosition { get; protected internal set; } BufferWriter SaveBuffer { get; protected internal set; } - int TypeRef { get; } Serial Serial { get; } // Executed on every entity, before it's serialized. @@ -39,11 +39,9 @@ public interface ISerializable void Delete(); bool Deleted { get; } - void SetTypeRef(Type type); - - public void InitializeSaveBuffer(byte[] buffer) + public void InitializeSaveBuffer(byte[] buffer, ConcurrentQueue types) { - SaveBuffer = new BufferWriter(buffer, true); + SaveBuffer = new BufferWriter(buffer, true, types); if (World.DirtyTrackingEnabled) { SavePosition = SaveBuffer.Position; @@ -54,9 +52,9 @@ public interface ISerializable } } - public void Serialize() + public void Serialize(ConcurrentQueue types) { - SaveBuffer ??= new BufferWriter(true); + SaveBuffer ??= new BufferWriter(true, types); BeforeSerialize(); diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index 048b087ba..e27b3fba4 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -14,13 +14,14 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace Server; -public class Persistence +public static class Persistence { public const int DefaultPriority = 100; @@ -30,7 +31,7 @@ public class Persistence string name, Action serializer, Action snapshotWriter, - Action deserializer, + Action> deserializer, int priority = DefaultPriority ) { @@ -50,24 +51,79 @@ public class Persistence public static void Load(string path) { + var typesDb = LoadTypes(path); + // This should probably not be parallel since Mobiles must be loaded before Items foreach (var entry in _registry) { - entry.Deserialize(path); + entry.Deserialize(path, typesDb); } } + private static Dictionary LoadTypes(string path) + { + var db = new Dictionary(); + + string tdbPath = Path.Combine(path, "SerializedTypes.db"); + if (!File.Exists(tdbPath)) + { + return db; + } + + using FileStream tdb = new FileStream(tdbPath, FileMode.Open, FileAccess.Read, FileShare.Read); + BinaryReader tdbReader = new BinaryReader(tdb); + + var version = tdbReader.ReadInt32(); + var count = tdbReader.ReadInt32(); + + for (var i = 0; i < count; ++i) + { + var hash = tdbReader.ReadUInt64(); + var typeName = tdbReader.ReadString(); + db[hash] = typeName; + } + + return db; + } + public static void Serialize() { Parallel.ForEach(_registry, entry => entry.Serialize()); } - public static void WriteSnapshot(string path) + public static void WriteSnapshot(string path, ConcurrentQueue types) { foreach (var entry in _registry) { entry.WriteSnapshot(path); } + + // Dedupe the queue. + foreach (var type in types) + { + _typesSet.Add(type); + } + + WriteSerializedTypesSnapshot(path, _typesSet); + _typesSet.Clear(); + } + + private static HashSet _typesSet = new(); + + public static void WriteSerializedTypesSnapshot(string path, HashSet types) + { + string tdbPath = Path.Combine(path, "SerializedTypes.db"); + using var tdb = new BinaryFileWriter(tdbPath, false); + + tdb.Write(0); // version + tdb.Write(types.Count); + + foreach (var type in types) + { + var fullName = type.FullName; + tdb.Write(HashUtility.ComputeHash64(fullName)); + tdb.Write(fullName); + } } public record RegistryEntry @@ -76,7 +132,7 @@ public class Persistence public int Priority { get; init; } public Action Serialize { get; init; } // Serializing to memory buffers public Action WriteSnapshot { get; init; } - public Action Deserialize { get; init; } + public Action> Deserialize { get; init; } } internal class RegistryEntryComparer : IComparer diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index db2d421d6..53ca56095 100755 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -38,6 +38,7 @@ + diff --git a/Projects/Server/Utilities/HashUtility.cs b/Projects/Server/Utilities/HashUtility.cs new file mode 100644 index 000000000..e757c5140 --- /dev/null +++ b/Projects/Server/Utilities/HashUtility.cs @@ -0,0 +1,48 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2022 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: HashUtility.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.Runtime.CompilerServices; +using Standart.Hash.xxHash; + +namespace Server; + +/// +/// Represents supported non-cryptographic fast hash algorithms. +/// +public enum FastHashAlgorithm +{ + None, // Used for collisions where full-data is serialized instead + XXHash3_64, // xxHash3 64bit +} + +public static class HashUtility +{ + // *************** DO NOT CHANGE THIS NUMBER **************** + // * Computed hashes might be serialized against this seed! * + // ********************************************************** + private const ulong xxHash3Seed = 9609125370673258709ul; // Randomly generated 64-bit prime number + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ComputeHash64(string? data, FastHashAlgorithm algorithm = FastHashAlgorithm.XXHash3_64) => + algorithm switch + { + FastHashAlgorithm.XXHash3_64 => ComputeXXHash3_64(data), + _ => throw new NotSupportedException($"Hash {algorithm} is not supported.") + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ComputeXXHash3_64(string? data) => data == null ? 0 : xxHash3.ComputeHash(data, xxHash3Seed); +} diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 0ef9c036c..21b986818 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -120,8 +120,10 @@ public static class Utility sb.Append(value); } - public static string Intern(string str) => str?.Length > 0 ? string.Intern(str) : str; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Intern(this string str) => str?.Length > 0 ? string.Intern(str) : str; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Intern(ref string str) { str = Intern(str); diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs index 8586ae7dd..69d9b37fa 100644 --- a/Projects/Server/World/EntityPersistence.cs +++ b/Projects/Server/World/EntityPersistence.cs @@ -14,6 +14,7 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; @@ -25,13 +26,11 @@ namespace Server; public static class EntityPersistence { - private const int _idxVersion = 1; - public static void WriteEntities( IIndexInfo indexInfo, Dictionary entities, - List types, string savePath, + ConcurrentQueue types, out Dictionary counts ) where T : class, ISerializable { @@ -44,20 +43,19 @@ public static class EntityPersistence PathUtility.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); + using var idx = new BinaryFileWriter(idxPath, false, types); + using var bin = new BinaryFileWriter(binPath, true, types); - idx.Write(1); // Version + idx.Write(2); // Version idx.Write(entities.Count); foreach (var e in entities.Values) { long start = bin.Position; - idx.Write(e.TypeRef); + var t = e.GetType(); + idx.Write(t); idx.Write(e.Serial); idx.Write(e.Created.Ticks); idx.Write(e.LastSerialized.Ticks); @@ -73,12 +71,6 @@ public static class EntityPersistence 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)] @@ -90,6 +82,7 @@ public static class EntityPersistence public static Dictionary LoadIndex( string path, IIndexInfo indexInfo, + Dictionary serializedTypes, out List> entities ) where T : class, ISerializable { @@ -99,11 +92,10 @@ public static class EntityPersistence 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)) + if (!File.Exists(indexPath)) { return map; } @@ -111,58 +103,77 @@ public static class EntityPersistence 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); - - int count; var version = idxReader.ReadInt32(); + int count = idxReader.ReadInt32(); - // Handle non-versioned (version 0). - if (version > _idxVersion || idx.Length - 4 - version * 20 == 0) + var ctorArguments = new[] { typeof(I) }; + List types; + + string typesPath = Path.Combine(path, indexType, $"{indexType}.tdb"); + if (File.Exists(typesPath)) { - count = version; - version = 0; + using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read); + BinaryReader tdbReader = new BinaryReader(tdb); + types = ReadTypes(tdbReader, ctorArguments); + tdbReader.Close(); } else { - count = idxReader.ReadInt32(); + types = null; + } + + // We must have a typeDb from SerializedTypes.db, or a tdb file + if (serializedTypes == null && types == null) + { + return map; } var now = DateTime.UtcNow; for (int i = 0; i < count; ++i) { - var typeID = idxReader.ReadInt32(); + ConstructorInfo ctor; + if (version >= 2) + { + var flag = idxReader.ReadByte(); + if (flag != 2) + { + throw new Exception($"Invalid type flag, expected 2 but received {flag}."); + } + + var hash = idxReader.ReadUInt64(); + serializedTypes!.TryGetValue(hash, out var typeName); + ctor = GetConstructorFor(typeName, AssemblyHandler.FindTypeByHash(hash), ctorArguments); + } + else + { + ctor = types?[idxReader.ReadInt32()]; + } + var serial = idxReader.ReadUInt32(); var created = version == 0 ? now : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc); var lastSerialized = version == 0 ? DateTime.MinValue : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc); var pos = idxReader.ReadInt64(); var length = idxReader.ReadInt32(); - Tuple objs = types[typeID]; - - if (objs == null) + if (ctor == null) { continue; } - ConstructorInfo ctor = objs.Item1; I indexer = indexInfo.CreateIndex(serial); ctorArgs[0] = indexer; - if (ctor.Invoke(ctorArgs) is T t) + if (ctor.Invoke(ctorArgs) is T entity) { - t.Created = created; - t.LastSerialized = lastSerialized; - entities.Add(new EntitySpan(t, typeID, pos, length)); - map[indexer] = t; + entity.Created = created; + entity.LastSerialized = lastSerialized; + entities.Add(new EntitySpan(entity, pos, length)); + map[indexer] = entity; } } - tdbReader.Close(); idxReader.Close(); return map; @@ -171,6 +182,7 @@ public static class EntityPersistence public static void LoadData( string path, IIndexInfo indexInfo, + Dictionary serializedTypes, List> entities ) where T : class, ISerializable { @@ -211,7 +223,7 @@ public static class EntityPersistence var buffer = GC.AllocateUninitializedArray(entry.Length); if (br == null) { - br = new BufferReader(buffer, t.LastSerialized); + br = new BufferReader(buffer, t.LastSerialized, serializedTypes); } else { @@ -236,7 +248,7 @@ public static class EntityPersistence if (error == null) { - t.InitializeSaveBuffer(buffer); + t.InitializeSaveBuffer(buffer, World.SerializedTypes); } else { @@ -265,50 +277,52 @@ public static class EntityPersistence } } - private static List> ReadTypes(BinaryReader tdbReader) + private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes) { - var constructorTypes = new[] { typeof(I) }; + if (t?.IsAbstract != false) + { + Console.WriteLine("failed"); + var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; + + Console.WriteLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)"); + + if (Console.ReadKey(true).Key == ConsoleKey.Y) + { + Console.WriteLine("Loading..."); + return null; + } + + Console.WriteLine("Types will not be deleted. An exception will be thrown."); + + throw new Exception($"Bad type '{typeName}'"); + } + + var ctor = t.GetConstructor(constructorTypes); + + if (ctor == null) + { + throw new Exception($"Type '{t}' does not have a serialization constructor"); + } + + return ctor; + } + + /** + * Legacy ReadTypes for backward compatibility with old saves that still have a tdb file + */ + private static List ReadTypes(BinaryReader tdbReader, Type[] ctorArguments) + { var count = tdbReader.ReadInt32(); - var types = new List>(count); + 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) - { - Console.WriteLine("failed"); - - var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; - - Console.WriteLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)"); - - if (Console.ReadKey(true).Key == ConsoleKey.Y) - { - types.Add(null); - Console.WriteLine("Loading..."); - continue; - } - - Console.WriteLine("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"); - } + var ctor = GetConstructorFor(typeName, t, ctorArguments); + types.Add(ctor); } return types; diff --git a/Projects/Server/World/EntitySpan.cs b/Projects/Server/World/EntitySpan.cs index 16f8670df..c01f06514 100644 --- a/Projects/Server/World/EntitySpan.cs +++ b/Projects/Server/World/EntitySpan.cs @@ -19,16 +19,13 @@ public struct EntitySpan where T : ISerializable { public T Entity { get; } - public int TypeID { get; } - public long Position { get; } public int Length { get; } - public EntitySpan(T entity, int typeID, long position, int length) + public EntitySpan(T entity, long position, int length) { Entity = entity; - TypeID = typeID; Position = position; Length = length; } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 818a67883..2c0893596 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -38,12 +38,12 @@ public enum WorldState public static class World { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(World)); + private static ILogger logger = LogFactory.GetLogger(typeof(World)); - private static readonly ManualResetEvent m_DiskWriteHandle = new(true); - private static readonly Dictionary _pendingAdd = new(); - private static readonly Dictionary _pendingDelete = new(); - private static readonly ConcurrentQueue _decayQueue = new(); + 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; @@ -125,10 +125,6 @@ public static class World private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); - internal static List ItemTypes { get; } = new(); - internal static List MobileTypes { get; } = new(); - internal static List GuildTypes { get; } = new(); - public static string SavePath { get; private set; } public static WorldState WorldState { get; private set; } @@ -232,15 +228,15 @@ public static class World public static void Broadcast(int hue, bool ascii, string format, params object[] args) => Broadcast(hue, ascii, string.Format(format, args)); - internal static void LoadEntities(string basePath) + 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, out List> mobiles); - Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, out List> items); - Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, out List> 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) { @@ -257,9 +253,9 @@ public static class World _lastGuild = Guilds.Keys.Max(); } - EntityPersistence.LoadData(basePath, mobileIndexInfo, mobiles); - EntityPersistence.LoadData(basePath, itemIndexInfo, items); - EntityPersistence.LoadData(basePath, guildIndexInfo, guilds); + EntityPersistence.LoadData(basePath, mobileIndexInfo, typesDb, mobiles); + EntityPersistence.LoadData(basePath, itemIndexInfo, typesDb, items); + EntityPersistence.LoadData(basePath, guildIndexInfo, typesDb, guilds); } public static void Load() @@ -392,9 +388,9 @@ public static class World IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - 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); + 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) { @@ -413,7 +409,7 @@ public static class World var watch = Stopwatch.StartNew(); logger.Information("Writing world save snapshot"); - Persistence.WriteSnapshot(tempPath); + Persistence.WriteSnapshot(tempPath, SerializedTypes); watch.Stop(); @@ -444,6 +440,9 @@ public static class World } } + // Clear types + SerializedTypes.Clear(); + m_DiskWriteHandle.Set(); Core.LoopContext.Post(FinishWorldSave); @@ -463,6 +462,39 @@ public static class World private static DateTime _serializationStart; + /** + * Duplicates can be weeded out asynchronously while flushing + * If performance becomes a problem, we need to build a dual mode concurrent array. + * + ****************************************************** Proposal ****************************************************** + * The structure is initialized with a large capacity to avoid unnecessary resizing. + * Write Mode: + * - Multiple threads can add a single, or a range of elements concurrently. + * - Elements can be Peeked, but there are no guarantees. + * - To resize the internal array, replaced it with the next size up from an array pool. + * - The structure cannot be cleared in this mode. + * + * Read Mode: + * - The array can be read from multiple threads using a ref struct enumerator. + * - Elements cannot be added or reassigned. + * - Cleared by replacing the internal array with another one from the pool. + * - Note: Upon clearing, the existing array is not sent back to the pool until there are zero enumerators. + * + * Enumeration: + * - Multiple threads can enumerate while in read mode. The enumerator will Interlocked.Increment a read counter. + * - Upon dispose of the enumerator, the read counter will be lowered with an Interlocked.Decrement + * - When the read counter reaches 0, if there is a cleared array, the array is sent back to the pool zeroed. + * + * Notes: + * - Elements can never be removed. + * + * How is this different from ConcurrentQueue? + * The functionality is very similar, except the constraints allow the implementation to be done without locks. + * Since this implementation uses pooled arrays, allocations will approach zero over time. + ********************************************************************************************************************** + */ + public static ConcurrentQueue SerializedTypes { get; } = new(); + internal static void SaveEntities() { _serializationStart = DateTime.UtcNow; @@ -479,7 +511,7 @@ public static class World EnqueueForDecay(item); } - entity.Serialize(); + entity.Serialize(SerializedTypes); } public static void Save() @@ -632,7 +664,10 @@ public static class World } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void AddGuild(BaseGuild guild) => Guilds[guild.Serial] = guild; + public static void AddGuild(BaseGuild guild) + { + Guilds[guild.Serial] = guild; + } public static void RemoveEntity(T entity) where T : class, IEntity { diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index 34ebc5ddb..731974cfc 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -15,6 +15,10 @@ namespace Server.Accounting [SerializationGenerator(4)] public partial class Account : IAccount, IComparable, ISerializable { + public void SetTypeRef(Type type) + { + } + 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); @@ -145,7 +149,6 @@ namespace Server.Accounting public Account(XmlElement node) { Serial = Accounts.NewAccount; - SetTypeRef(GetType()); _username = Utility.GetText(node["username"], "empty"); @@ -218,17 +221,6 @@ namespace Server.Accounting this.MarkDirty(); } - public void SetTypeRef(Type type) - { - TypeRef = Accounts.Types.IndexOf(type); - - if (TypeRef == -1) - { - Accounts.Types.Add(type); - TypeRef = Accounts.Types.Count - 1; - } - } - /// /// Object detailing information about the hardware of the last person to log into this account /// @@ -305,8 +297,6 @@ namespace Server.Accounting [CommandProperty(AccessLevel.GameMaster)] DateTime ISerializable.LastSerialized { get; set; } = Core.Now; - public int TypeRef { get; private set; } - public Serial Serial { get; set; } public void BeforeSerialize() diff --git a/Projects/UOContent/Accounting/Accounts.cs b/Projects/UOContent/Accounting/Accounts.cs index 0d81363b9..b71dfad7e 100644 --- a/Projects/UOContent/Accounting/Accounts.cs +++ b/Projects/UOContent/Accounting/Accounts.cs @@ -14,7 +14,6 @@ namespace Server.Accounting 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(); private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); @@ -44,13 +43,18 @@ namespace Server.Accounting public static void Configure() => Persistence.Register("Accounts", Serialize, WriteSnapshot, Deserialize); - internal static void Serialize() => - EntityPersistence.SaveEntities(_accountsById.Values, account => ((ISerializable)account).Serialize()); + 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, Types, basePath, out _); + EntityPersistence.WriteEntities(indexInfo, _accountsById, basePath,World.SerializedTypes, out _); } public static IEnumerable GetAccounts() => _accountsByName.Values; @@ -73,7 +77,7 @@ namespace Server.Accounting _accountsById.Remove(a.Serial); } - internal static void Deserialize(string path) + internal static void Deserialize(string path, Dictionary typesDb) { var filePath = Path.Combine(path, "Accounts", "accounts.xml"); @@ -86,14 +90,14 @@ namespace Server.Accounting IIndexInfo indexInfo = new EntityTypeIndex("Accounts"); - _accountsById = EntityPersistence.LoadIndex(path, indexInfo, out List> accounts); + _accountsById = EntityPersistence.LoadIndex(path, indexInfo, typesDb, out List> accounts); if (_accountsById.Count > 0) { _lastAccount = _accountsById.Keys.Max(); } - EntityPersistence.LoadData(path, indexInfo, accounts); + EntityPersistence.LoadData(path, indexInfo, typesDb, accounts); foreach (var a in _accountsById.Values) { diff --git a/Projects/UOContent/Items/Armor/Cloth/GargishClothKiltType2.cs b/Projects/UOContent/Items/Armor/Cloth/GargishClothKiltType2.cs index 45c836fb4..8f9e4a486 100644 --- a/Projects/UOContent/Items/Armor/Cloth/GargishClothKiltType2.cs +++ b/Projects/UOContent/Items/Armor/Cloth/GargishClothKiltType2.cs @@ -3,7 +3,6 @@ using ModernUO.Serialization; namespace Server.Items { [SerializationGenerator(0)] - [TypeAlias("Server.Items.GargishClothKilt", "Server.Items.GargishClothKiltArmor")] public partial class GargishClothKiltType2 : BaseArmor { [Constructible] diff --git a/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/SkeletalDragon.cs b/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/SkeletalDragon.cs index 774bf438c..e3d5ebb6f 100644 --- a/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/SkeletalDragon.cs +++ b/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/SkeletalDragon.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Server.Mobiles { public class SkeletalDragon : BaseCreature diff --git a/Projects/UOContent/Spells/Chivalry/NobleSacrifice.cs b/Projects/UOContent/Spells/Chivalry/NobleSacrifice.cs index d6e9f4a79..8b67f9b6b 100644 --- a/Projects/UOContent/Spells/Chivalry/NobleSacrifice.cs +++ b/Projects/UOContent/Spells/Chivalry/NobleSacrifice.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Server.Collections; using Server.Gumps; using Server.Items; diff --git a/version.json b/version.json index 44aefe8f2..00b0a5ef6 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.9.3" + "version": "0.9.4" }