fix(serialization): Updates serialization flow & fixes serializing accounts (#521)
This commit is contained in:
parent
78e3450671
commit
8d54d273a4
10 changed files with 747 additions and 381 deletions
|
|
@ -88,7 +88,7 @@ namespace Server.Accounting
|
|||
long GetTotalGold();
|
||||
}
|
||||
|
||||
public interface IAccount : IGoldAccount, IComparable<IAccount>
|
||||
public interface IAccount : IGoldAccount, IComparable<IAccount>, ISerializable
|
||||
{
|
||||
string Username { get; set; }
|
||||
string Email { get; set; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
54
Projects/Server/Serialization/GenericPersistence.cs
Normal file
54
Projects/Server/Serialization/GenericPersistence.cs
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
public static class GenericPersistence
|
||||
{
|
||||
public static void Serialize(Action<IGenericWriter> serializer) => serializer(new BufferWriter(true));
|
||||
|
||||
public static void WriteSnapshot(string path, Action<IGenericWriter> 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<IGenericReader> 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<byte>((int)fs.Length);
|
||||
|
||||
deserializer(new BufferReader(buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IGenericWriter> serializer)
|
||||
public static readonly SortedSet<RegistryEntry> _registry = new(new RegistryEntryComparer());
|
||||
|
||||
public static void Register(
|
||||
Action serializer,
|
||||
Action<string> snapshotWriter,
|
||||
Action<string> 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<IGenericReader> 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<byte>((int)fs.Length);
|
||||
public class RegistryEntry
|
||||
{
|
||||
public int Priority { get; init; }
|
||||
public Action Serialize { get; init; } // Serializing to memory buffers
|
||||
public Action<string> WriteSnapshot { get; init; }
|
||||
public Action<string> Deserialize { get; init; }
|
||||
}
|
||||
|
||||
internal class RegistryEntryComparer : IComparer<RegistryEntry>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
285
Projects/Server/World/EntityPersistence.cs
Normal file
285
Projects/Server/World/EntityPersistence.cs
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
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<I, T>(
|
||||
IIndexInfo<I> indexInfo,
|
||||
Dictionary<I, T> entities,
|
||||
List<Type> types,
|
||||
string savePath,
|
||||
out Dictionary<string, int> counts
|
||||
) where T : class, ISerializable
|
||||
{
|
||||
counts = new Dictionary<string, int>();
|
||||
|
||||
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<T>(
|
||||
IEnumerable<T> list,
|
||||
Action<T> serializer
|
||||
) where T : class, ISerializable => Parallel.ForEach(list, serializer);
|
||||
|
||||
public static Dictionary<I, T> LoadIndex<I, T>(
|
||||
string path,
|
||||
IIndexInfo<I> indexInfo,
|
||||
out List<EntityIndex<T>> entities
|
||||
) where T : class, ISerializable
|
||||
{
|
||||
var map = new Dictionary<I, T>();
|
||||
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<EntityIndex<T>>();
|
||||
|
||||
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<Tuple<ConstructorInfo, string>> types = ReadTypes<I>(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<ConstructorInfo, string> 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>(t, typeID, pos, length));
|
||||
map[indexer] = t;
|
||||
}
|
||||
}
|
||||
|
||||
tdbReader.Close();
|
||||
idxReader.Close();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static void LoadData<I, T>(
|
||||
string path,
|
||||
IIndexInfo<I> indexInfo,
|
||||
List<EntityIndex<T>> 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<byte>(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<Tuple<ConstructorInfo, string>> ReadTypes<I>(BinaryReader tdbReader)
|
||||
{
|
||||
var constructorTypes = new[] { typeof(I) };
|
||||
|
||||
var count = tdbReader.ReadInt32();
|
||||
|
||||
var types = new List<Tuple<ConstructorInfo, string>>(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<ConstructorInfo, string>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Tuple<ConstructorInfo, string>> ReadTypes<I>(BinaryReader tdbReader)
|
||||
internal static void LoadEntities(string basePath)
|
||||
{
|
||||
var constructorTypes = new[] { typeof(I) };
|
||||
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
|
||||
|
||||
var count = tdbReader.ReadInt32();
|
||||
Mobiles = EntityPersistence.LoadIndex(basePath, mobileIndexInfo, out List<EntityIndex<Mobile>> mobiles);
|
||||
Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, out List<EntityIndex<Item>> items);
|
||||
Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, out List<EntityIndex<BaseGuild>> guilds);
|
||||
|
||||
var types = new List<Tuple<ConstructorInfo, string>>(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<ConstructorInfo, string>(ctor, typeName));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Type '{t}' does not have a serialization constructor");
|
||||
}
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
private static Dictionary<I, T> LoadIndex<I, T>(IIndexInfo<I> indexInfo, out List<EntityIndex<T>> entities) where T : class, ISerializable
|
||||
{
|
||||
var map = new Dictionary<I, T>();
|
||||
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<EntityIndex<T>>();
|
||||
|
||||
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<Tuple<ConstructorInfo, string>> types = ReadTypes<I>(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<ConstructorInfo, string> 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>(t, typeID, pos, length));
|
||||
map[indexer] = t;
|
||||
}
|
||||
}
|
||||
|
||||
tdbReader.Close();
|
||||
idxReader.Close();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void LoadData<I, T>(IIndexInfo<I> indexInfo, List<EntityIndex<T>> 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<byte>(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<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
|
||||
|
||||
Mobiles = LoadIndex(mobileIndexInfo, out List<EntityIndex<Mobile>> mobiles);
|
||||
Items = LoadIndex(itemIndexInfo, out List<EntityIndex<Item>> items);
|
||||
Guilds = LoadIndex(guildIndexInfo, out List<EntityIndex<BaseGuild>> 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<KeyValuePair<string, int>>[] entityTypes)
|
||||
{
|
||||
try
|
||||
|
|
@ -559,17 +360,22 @@ namespace Server
|
|||
}
|
||||
}
|
||||
|
||||
public static void WriteFiles(object state)
|
||||
internal static void WriteEntities(string basePath)
|
||||
{
|
||||
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> 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<string, int> mobileCounts = null;
|
||||
Dictionary<string, int> itemCounts = null;
|
||||
Dictionary<string, int> 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<I, T>(
|
||||
IIndexInfo<I> indexInfo,
|
||||
Dictionary<I, T> entities,
|
||||
List<Type> types,
|
||||
string savePath,
|
||||
out Dictionary<string, int> counts
|
||||
) where T : class, ISerializable
|
||||
{
|
||||
counts = new Dictionary<string, int>();
|
||||
|
||||
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<T>(IEnumerable<T> 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>(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;
|
||||
|
|
|
|||
|
|
@ -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<AccountComment> m_Comments;
|
||||
private PasswordProtectionAlgorithm m_PasswordAlgorithm;
|
||||
private List<AccountTag> 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
|
|||
/// <summary>
|
||||
/// The date and time of when this account was created.
|
||||
/// </summary>
|
||||
public DateTime Created { get; }
|
||||
public DateTime Created { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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<Mobile>();
|
||||
}
|
||||
|
||||
length = reader.ReadInt();
|
||||
m_Comments = length > 0 ? new List<AccountComment>(length) : null;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
m_Comments!.Add(new AccountComment(reader));
|
||||
}
|
||||
|
||||
length = reader.ReadInt();
|
||||
m_Tags = length > 0 ? new List<AccountTag>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the account, all characters of the account, and all houses of those characters
|
||||
/// </summary>
|
||||
|
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Account username. Case insensitive validation.
|
||||
/// </summary>
|
||||
|
|
@ -784,7 +952,7 @@ namespace Server.Accounting
|
|||
/// </summary>
|
||||
/// <param name="node">The XmlElement from which to deserialize.</param>
|
||||
/// <returns>String list. Value will never be null.</returns>
|
||||
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
|
|||
/// </summary>
|
||||
/// <param name="node">The XmlElement from which to deserialize.</param>
|
||||
/// <returns>Address list. Value will never be null.</returns>
|
||||
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
|
|||
/// </summary>
|
||||
/// <param name="node">The XmlElement instance from which to deserialize.</param>
|
||||
/// <returns>Mobile list. Value will never be null.</returns>
|
||||
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
|
|||
/// </summary>
|
||||
/// <param name="node">The XmlElement from which to deserialize.</param>
|
||||
/// <returns>Comment list. Value will never be null.</returns>
|
||||
public static List<AccountComment> LoadComments(XmlElement node)
|
||||
private static List<AccountComment> LoadComments(XmlElement node)
|
||||
{
|
||||
List<AccountComment> list = null;
|
||||
var comments = node["comments"];
|
||||
|
|
@ -935,7 +1103,7 @@ namespace Server.Accounting
|
|||
/// </summary>
|
||||
/// <param name="node">The XmlElement from which to deserialize.</param>
|
||||
/// <returns>Tag list. Value will never be null.</returns>
|
||||
public static List<AccountTag> LoadTags(XmlElement node)
|
||||
private static List<AccountTag> LoadTags(XmlElement node)
|
||||
{
|
||||
List<AccountTag> list = null;
|
||||
var tags = node["tags"];
|
||||
|
|
|
|||
|
|
@ -30,6 +30,17 @@ namespace Server.Accounting
|
|||
m_Content = Utility.GetText(node, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an AccountComment instance.
|
||||
/// </summary>
|
||||
/// <param name="node">The deserialization reader</param>
|
||||
public AccountComment(IGenericReader reader)
|
||||
{
|
||||
AddedBy = reader.ReadString();
|
||||
LastModified = reader.ReadDateTime();
|
||||
m_Content = reader.ReadString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A string representing who added this comment.
|
||||
/// </summary>
|
||||
|
|
@ -69,5 +80,16 @@ namespace Server.Accounting
|
|||
|
||||
xml.WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes this AccountComment instance.
|
||||
/// </summary>
|
||||
/// <param name="xml">The serialization writer.</param>
|
||||
public void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.Write(AddedBy ?? "empty");
|
||||
writer.Write(LastModified);
|
||||
writer.Write(m_Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ namespace Server.Accounting
|
|||
Value = Utility.GetText(node, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an AccountTag instance .
|
||||
/// </summary>
|
||||
/// <param name="node">The deserialization reader</param>
|
||||
public AccountTag(IGenericReader reader)
|
||||
{
|
||||
Name = reader.ReadString();
|
||||
Value = reader.ReadString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of this tag.
|
||||
/// </summary>
|
||||
|
|
@ -46,5 +56,15 @@ namespace Server.Accounting
|
|||
xml.WriteString(Value);
|
||||
xml.WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes this AccountTag instance to an XmlTextWriter.
|
||||
/// </summary>
|
||||
/// <param name="xml">The serialization writer.</param>
|
||||
public void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.Write(Name ?? "empty");
|
||||
writer.Write(Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,50 +7,92 @@ namespace Server.Accounting
|
|||
{
|
||||
public static class Accounts
|
||||
{
|
||||
private static Dictionary<string, IAccount> m_Accounts = new();
|
||||
private static readonly Dictionary<string, IAccount> _accountsByName = new(32, StringComparer.OrdinalIgnoreCase);
|
||||
private static Dictionary<Serial, IAccount> _accountsById = new(32);
|
||||
private static Serial _lastAccount;
|
||||
internal static List<Type> 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<Serial> indexInfo = new EntityTypeIndex("Accounts");
|
||||
EntityPersistence.WriteEntities(indexInfo, _accountsById, Types, basePath, out _);
|
||||
}
|
||||
|
||||
public static IEnumerable<IAccount> GetAccounts() => m_Accounts.Values;
|
||||
public static IEnumerable<IAccount> 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<string, IAccount>(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<Serial> indexInfo = new EntityTypeIndex("Accounts");
|
||||
|
||||
_accountsById = EntityPersistence.LoadIndex(path, indexInfo, out List<EntityIndex<IAccount>> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue