The idx v4 type table supersedes the global type queue: workers no longer enqueue every Write(Type) into a shared ConcurrentQueue during the freeze, WriteFiles no longer drains and dedupes millions of entries, and the db file is no longer produced. Loading old saves still reads SerializedTypes.db via the legacy v2/v3 path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
193 lines
5.7 KiB
C#
193 lines
5.7 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: Persistence.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.IO.MemoryMappedFiles;
|
|
|
|
namespace Server;
|
|
|
|
public abstract class Persistence
|
|
{
|
|
private static readonly SortedSet<Persistence> _registry = new(new PersistenceComparer());
|
|
|
|
public int Priority { get; }
|
|
|
|
public Persistence(int priority = 100)
|
|
{
|
|
Priority = priority;
|
|
_registry.Add(this);
|
|
}
|
|
|
|
public bool Register() => _registry.Add(this);
|
|
|
|
public void Unregister() => _registry.Remove(this);
|
|
|
|
public static void Load(string path)
|
|
{
|
|
var typesDb = LoadTypes(path);
|
|
|
|
foreach (var entry in _registry)
|
|
{
|
|
(entry as IGenericEntityPersistence)?.DeserializeIndexes(path, typesDb);
|
|
}
|
|
|
|
// This should probably not be parallel since Mobiles must be loaded before Items
|
|
foreach (var entry in _registry)
|
|
{
|
|
entry.Deserialize(path, typesDb);
|
|
}
|
|
}
|
|
|
|
private static unsafe Dictionary<ulong, string> LoadTypes(string path)
|
|
{
|
|
var db = new Dictionary<ulong, string>();
|
|
|
|
var typesPath = Path.Combine(path, "SerializedTypes.db");
|
|
if (!File.Exists(typesPath))
|
|
{
|
|
return db;
|
|
}
|
|
|
|
using var mmf = MemoryMappedFile.CreateFromFile(typesPath, FileMode.Open);
|
|
using var accessor = mmf.CreateViewStream();
|
|
|
|
byte* ptr = null;
|
|
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
|
var dataReader = new UnmanagedDataReader(ptr, accessor.Length);
|
|
|
|
var version = dataReader.ReadInt();
|
|
var count = dataReader.ReadInt();
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var hash = dataReader.ReadULong();
|
|
var typeName = dataReader.ReadStringRaw();
|
|
db[hash] = typeName;
|
|
}
|
|
|
|
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
|
|
return db;
|
|
}
|
|
|
|
// Note: This is strictly on a background thread
|
|
internal static void WriteSnapshotAll(string path)
|
|
{
|
|
foreach (var p in _registry)
|
|
{
|
|
p.WriteSnapshot(path);
|
|
}
|
|
}
|
|
|
|
internal static void SerializeAll()
|
|
{
|
|
// Largest known payloads first (LPT scheduling). An indivisible multi-megabyte system
|
|
// serialized last extends the freeze by its entire duration; serialized first it overlaps
|
|
// the entity stream. Sizes come from the previous save, or the loaded files on first save.
|
|
var ordered = new Persistence[_registry.Count];
|
|
_registry.CopyTo(ordered);
|
|
Array.Sort(ordered, static (a, b) => GetEstimatedSize(b).CompareTo(GetEstimatedSize(a)));
|
|
|
|
foreach (var p in ordered)
|
|
{
|
|
// Chunks are persistence-homogeneous: publishing the partial chunk at each
|
|
// boundary lets workers attribute buffer-chunk records to their owner without
|
|
// any per-entity state.
|
|
World.SetChunkSourceOwner(p);
|
|
p.Serialize();
|
|
}
|
|
}
|
|
|
|
private static long GetEstimatedSize(Persistence p) => (p as GenericPersistence)?.EstimatedSize ?? 0;
|
|
|
|
internal static void PostWorldSaveAll()
|
|
{
|
|
foreach (var p in _registry)
|
|
{
|
|
p.PostWorldSave();
|
|
}
|
|
}
|
|
|
|
internal static void PostDeserializeAll()
|
|
{
|
|
foreach (var p in _registry)
|
|
{
|
|
p.PostDeserialize();
|
|
}
|
|
}
|
|
|
|
// Note: This should only be run on a background thread
|
|
public abstract void WriteSnapshot(string savePath);
|
|
|
|
public abstract void Serialize();
|
|
|
|
public abstract void Deserialize(string savePath, Dictionary<ulong, string> typesDb);
|
|
|
|
public virtual void PostWorldSave()
|
|
{
|
|
}
|
|
|
|
public virtual void PostDeserialize()
|
|
{
|
|
}
|
|
|
|
internal class PersistenceComparer : IComparer<Persistence>
|
|
{
|
|
public int Compare(Persistence x, Persistence y)
|
|
{
|
|
if (x == y)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (x == null)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (y == null)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
// First sort by priority
|
|
var cmp = x.Priority.CompareTo(y.Priority);
|
|
|
|
// Then alphabetically. We won't allow the same entry (by name) twice in the SortedSet
|
|
return cmp != 0 ? cmp : x.GetHashCode().CompareTo(y.GetHashCode());
|
|
}
|
|
}
|
|
|
|
public static void TraceException(Exception ex)
|
|
{
|
|
try
|
|
{
|
|
using var op = new StreamWriter("save-errors.log", true);
|
|
op.WriteLine("# {0}", Core.Now);
|
|
|
|
op.WriteLine(ex);
|
|
|
|
op.WriteLine();
|
|
op.WriteLine();
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
Console.WriteLine(ex);
|
|
}
|
|
}
|