fix: Cleans up entity persistence. Generalizes Mobiles/Items/Guilds (#1528)
### Summary - [X] Fixed a bug where entity persistence was serialized out of order, causing world corruption. - [X] Fixed LastSerialized not being utilized properly and dangling references still becoming an issue. - [X] Added a new `GenericEntityPersistence<T>` type to encapsulate `ISerializable` serialization. - [X] Removing the custom logic and moved Items, Mobiles, Guilds, and Accounts to GenericEntityPersistence. - [X] Changed serialization to use the singleton pattern to reduce calling methods from stored variables.
This commit is contained in:
parent
e0225c6e59
commit
f2de2fbb77
34 changed files with 721 additions and 970 deletions
|
|
@ -51,7 +51,7 @@ public static class AdhocPersistence
|
|||
|
||||
/**
|
||||
* Serializes to a memory buffer synchronously, then flushes to the path asynchronously.
|
||||
* See WriteSnapshot for more info about how the snapshot.
|
||||
* See WriteSnapshot for more info about how to snapshot.
|
||||
*/
|
||||
public static void SerializeAndSnapshot(string filePath, Action<IGenericWriter> serializer, ConcurrentQueue<Type> types = null)
|
||||
{
|
||||
|
|
|
|||
311
Projects/Server/Serialization/GenericEntityPersistence.cs
Normal file
311
Projects/Server/Serialization/GenericEntityPersistence.cs
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2023 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: GenericEntityPersistence.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server;
|
||||
|
||||
public interface IGenericEntityPersistence
|
||||
{
|
||||
public void DeserializeIndexes(string savePath, Dictionary<ulong, string> typesDb);
|
||||
}
|
||||
|
||||
public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistence where T : class, ISerializable
|
||||
{
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntityPersistence<T>));
|
||||
|
||||
private static List<EntitySpan<T>> _entities;
|
||||
|
||||
private string _name;
|
||||
private Serial _lastEntitySerial;
|
||||
private readonly Dictionary<Serial, T> _pendingAdd = new();
|
||||
private readonly Dictionary<Serial, T> _pendingDelete = new();
|
||||
private uint _minSerial;
|
||||
private uint _maxSerial;
|
||||
|
||||
public Dictionary<Serial, T> EntitiesBySerial { get; private set; } = new();
|
||||
|
||||
public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : base(priority)
|
||||
{
|
||||
_name = name;
|
||||
_minSerial = minSerial;
|
||||
_maxSerial = maxSerial;
|
||||
typeof(T).RegisterFindEntity(Find);
|
||||
}
|
||||
|
||||
public override void Serialize()
|
||||
{
|
||||
// TODO: Hand off to a scheduler instead
|
||||
Parallel.ForEach(EntitiesBySerial.Values, SerializeEntity);
|
||||
}
|
||||
|
||||
protected virtual void SerializeEntity(T entity)
|
||||
{
|
||||
entity.Serialize(World.SerializedTypes);
|
||||
}
|
||||
|
||||
public override void WriteSnapshot(string basePath)
|
||||
{
|
||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex(_name);
|
||||
EntityPersistence.WriteEntities(indexInfo, EntitiesBySerial, basePath,World.SerializedTypes, out _);
|
||||
}
|
||||
|
||||
public virtual void DeserializeIndexes(string savePath, Dictionary<ulong, string> typesDb)
|
||||
{
|
||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex(_name);
|
||||
|
||||
EntitiesBySerial = EntityPersistence.LoadIndex(savePath, indexInfo, typesDb, out _entities);
|
||||
|
||||
if (EntitiesBySerial.Count > 0)
|
||||
{
|
||||
_lastEntitySerial = EntitiesBySerial.Keys.Max();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Deserialize(string savePath, Dictionary<ulong, string> typesDb)
|
||||
{
|
||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex(_name);
|
||||
EntityPersistence.LoadData(savePath, indexInfo, typesDb, _entities);
|
||||
_entities = null;
|
||||
}
|
||||
|
||||
public override void PostSerialize()
|
||||
{
|
||||
ProcessSafetyQueues();
|
||||
}
|
||||
|
||||
public override void PostDeserialize()
|
||||
{
|
||||
ProcessSafetyQueues();
|
||||
}
|
||||
|
||||
public Serial NewEntity
|
||||
{
|
||||
get
|
||||
{
|
||||
#if THREADGUARD
|
||||
if (Thread.CurrentThread != Core.Thread)
|
||||
{
|
||||
logger.Error(
|
||||
"Attempted to get a new entity serial from the wrong thread!\n{StackTrace}",
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
#endif
|
||||
var last = _lastEntitySerial;
|
||||
var max = (Serial)_maxSerial;
|
||||
|
||||
for (uint i = 0; i < _maxSerial; i++)
|
||||
{
|
||||
last++;
|
||||
|
||||
if (last > max)
|
||||
{
|
||||
last = (Serial)_minSerial;
|
||||
}
|
||||
|
||||
if (FindEntity<T>(last) == null)
|
||||
{
|
||||
return _lastEntitySerial = last;
|
||||
}
|
||||
}
|
||||
|
||||
OutOfMemory($"No serials left to allocate for {_name}");
|
||||
return Serial.MinusOne;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEntity(T entity)
|
||||
{
|
||||
var worldState = World.WorldState;
|
||||
switch (worldState)
|
||||
{
|
||||
default: // Not Running
|
||||
{
|
||||
throw new Exception($"Added {entity.GetType().Name} before world load.");
|
||||
}
|
||||
case WorldState.Saving:
|
||||
{
|
||||
AppendSafetyLog("add", entity);
|
||||
goto case WorldState.WritingSave;
|
||||
}
|
||||
case WorldState.Loading:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
if (_pendingDelete.Remove(entity.Serial))
|
||||
{
|
||||
logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString());
|
||||
}
|
||||
|
||||
_pendingAdd[entity.Serial] = entity;
|
||||
break;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(EntitiesBySerial, entity.Serial, out bool exists);
|
||||
if (exists)
|
||||
{
|
||||
if (entityEntry == entity)
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
entityEntry.GetType().FullName,
|
||||
entityEntry.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
entityEntry = entity;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEntity(T entity)
|
||||
{
|
||||
var worldState = World.WorldState;
|
||||
switch (worldState)
|
||||
{
|
||||
default: // Not Running
|
||||
{
|
||||
throw new Exception($"Removed {entity.GetType().Name} before world load.");
|
||||
}
|
||||
case WorldState.Saving:
|
||||
{
|
||||
AppendSafetyLog("delete", entity);
|
||||
goto case WorldState.WritingSave;
|
||||
}
|
||||
case WorldState.Loading:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
_pendingAdd.Remove(entity.Serial);
|
||||
_pendingDelete[entity.Serial] = entity;
|
||||
break;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
EntitiesBySerial.Remove(entity.Serial);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSafetyQueues()
|
||||
{
|
||||
foreach (var entity in _pendingAdd.Values)
|
||||
{
|
||||
AddEntity(entity);
|
||||
}
|
||||
|
||||
_pendingAdd.Clear();
|
||||
|
||||
foreach (var entity in _pendingDelete.Values)
|
||||
{
|
||||
if (_pendingAdd.ContainsKey(entity.Serial))
|
||||
{
|
||||
logger.Warning("Entity {Entity} was both pending deletion and addition after save", entity);
|
||||
}
|
||||
|
||||
RemoveEntity(entity);
|
||||
}
|
||||
|
||||
_pendingDelete.Clear();
|
||||
}
|
||||
|
||||
private void AppendSafetyLog(string action, ISerializable entity)
|
||||
{
|
||||
var message =
|
||||
$"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected.";
|
||||
|
||||
logger.Information(message, action, entity);
|
||||
|
||||
try
|
||||
{
|
||||
using var op = new StreamWriter("world-save-errors.log", true);
|
||||
op.WriteLine("{0}\t{1}", DateTime.UtcNow, message);
|
||||
op.WriteLine(new StackTrace(2).ToString());
|
||||
op.WriteLine();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Find(Serial serial) => FindEntity<T>(serial, false, false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Find(Serial serial, bool returnDeleted) => FindEntity<T>(serial, returnDeleted, false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity<T>(serial, returnDeleted, returnPending);
|
||||
|
||||
public R FindEntity<R>(Serial serial) where R : class, T => FindEntity<R>(serial, false, false);
|
||||
|
||||
public R FindEntity<R>(Serial serial, bool returnDeleted, bool returnPending) where R : class, T
|
||||
{
|
||||
switch (World.WorldState)
|
||||
{
|
||||
default: return null;
|
||||
case WorldState.Loading:
|
||||
case WorldState.Saving:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity))
|
||||
{
|
||||
return entity as R;
|
||||
}
|
||||
|
||||
if (returnPending && _pendingAdd.TryGetValue(serial, out entity) ||
|
||||
EntitiesBySerial.TryGetValue(serial, out entity))
|
||||
{
|
||||
return entity as R;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
return EntitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void OutOfMemory(string message) => throw new OutOfMemoryException(message);
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server;
|
||||
|
||||
public class GenericEntitySerialization<T> where T : class, ISerializable
|
||||
{
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntitySerialization<T>));
|
||||
|
||||
private static string _systemName;
|
||||
private static Serial _lastEntitySerial;
|
||||
private static readonly Dictionary<Serial, T> _pendingAdd = new();
|
||||
private static readonly Dictionary<Serial, T> _pendingDelete = new();
|
||||
private static Dictionary<Serial, T> _entitiesBySerial = new();
|
||||
|
||||
public static void Configure(string systemName)
|
||||
{
|
||||
_systemName = systemName;
|
||||
typeof(T).RegisterFindEntity(Find);
|
||||
Persistence.Register(_systemName, Serialize, WriteSnapshot, Deserialize);
|
||||
}
|
||||
|
||||
internal static void Serialize()
|
||||
{
|
||||
EntityPersistence.SaveEntities(
|
||||
_entitiesBySerial.Values,
|
||||
entity => entity.Serialize(World.SerializedTypes)
|
||||
);
|
||||
}
|
||||
|
||||
internal static void WriteSnapshot(string basePath)
|
||||
{
|
||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex(_systemName);
|
||||
EntityPersistence.WriteEntities(indexInfo, _entitiesBySerial, basePath,World.SerializedTypes, out _);
|
||||
}
|
||||
|
||||
internal static void Deserialize(string path, Dictionary<ulong, string> typesDb)
|
||||
{
|
||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex(_systemName);
|
||||
|
||||
_entitiesBySerial = EntityPersistence.LoadIndex(path, indexInfo, typesDb, out List<EntitySpan<T>> entities);
|
||||
|
||||
if (_entitiesBySerial.Count > 0)
|
||||
{
|
||||
_lastEntitySerial = _entitiesBySerial.Keys.Max();
|
||||
}
|
||||
|
||||
EntityPersistence.LoadData(path, indexInfo, typesDb, entities);
|
||||
}
|
||||
|
||||
public static Serial NewEntity
|
||||
{
|
||||
get
|
||||
{
|
||||
#if THREADGUARD
|
||||
if (Thread.CurrentThread != Core.Thread)
|
||||
{
|
||||
logger.Error(
|
||||
"Attempted to get a new entity serial from the wrong thread!\n{StackTrace}",
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
#endif
|
||||
var last = _lastEntitySerial;
|
||||
|
||||
for (uint i = 0; i < uint.MaxValue; i++)
|
||||
{
|
||||
last++;
|
||||
|
||||
if (FindEntity<T>(last) == null)
|
||||
{
|
||||
return _lastEntitySerial = last;
|
||||
}
|
||||
}
|
||||
|
||||
OutOfMemory("No serials left to allocate for BOBEntries");
|
||||
return Serial.MinusOne;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddEntity(T entity)
|
||||
{
|
||||
var worldState = World.WorldState;
|
||||
switch (worldState)
|
||||
{
|
||||
default: // Not Running
|
||||
{
|
||||
throw new Exception($"Added {entity.GetType().Name} before world load.");
|
||||
}
|
||||
case WorldState.Saving:
|
||||
case WorldState.Loading:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
if (_pendingDelete.Remove(entity.Serial))
|
||||
{
|
||||
logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString());
|
||||
}
|
||||
|
||||
_pendingAdd[entity.Serial] = entity;
|
||||
break;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(_entitiesBySerial, entity.Serial, out bool exists);
|
||||
if (exists)
|
||||
{
|
||||
if (entityEntry == entity)
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
entityEntry.GetType().FullName,
|
||||
entityEntry.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
entityEntry = entity;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveEntity(T entity)
|
||||
{
|
||||
var worldState = World.WorldState;
|
||||
switch (worldState)
|
||||
{
|
||||
default: // Not Running
|
||||
{
|
||||
throw new Exception($"Removed {entity.GetType().Name} before world load.");
|
||||
}
|
||||
case WorldState.Saving:
|
||||
case WorldState.Loading:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
_pendingAdd.Remove(entity.Serial);
|
||||
_pendingDelete[entity.Serial] = entity;
|
||||
break;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
_entitiesBySerial.Remove(entity.Serial);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Find(Serial serial) => FindEntity<T>(serial, false, false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Find(Serial serial, bool returnDeleted) => FindEntity<T>(serial, returnDeleted, false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity<T>(serial, returnDeleted, returnPending);
|
||||
|
||||
public static R FindEntity<R>(Serial serial) where R : class, T => FindEntity<R>(serial, false, false);
|
||||
|
||||
public static R FindEntity<R>(Serial serial, bool returnDeleted, bool returnPending) where R : class, T
|
||||
{
|
||||
switch (World.WorldState)
|
||||
{
|
||||
default: return null;
|
||||
case WorldState.Loading:
|
||||
case WorldState.Saving:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity))
|
||||
{
|
||||
return entity as R;
|
||||
}
|
||||
|
||||
if (returnPending && _pendingAdd.TryGetValue(serial, out entity) ||
|
||||
_entitiesBySerial.TryGetValue(serial, out entity))
|
||||
{
|
||||
return entity as R;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
return _entitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void OutOfMemory(string message) => throw new OutOfMemoryException(message);
|
||||
}
|
||||
|
|
@ -19,35 +19,33 @@ using System.IO;
|
|||
|
||||
namespace Server;
|
||||
|
||||
public static class GenericPersistence
|
||||
public abstract class GenericPersistence : Persistence
|
||||
{
|
||||
public static void Register(
|
||||
string name,
|
||||
Action<IGenericWriter> serializer,
|
||||
Action<IGenericReader> deserializer,
|
||||
int priority = Persistence.DefaultPriority
|
||||
)
|
||||
private BufferWriter _saveBuffer;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public GenericPersistence(string name, int priority) : base(priority) => Name = name;
|
||||
|
||||
public override void Serialize()
|
||||
{
|
||||
BufferWriter saveBuffer = null;
|
||||
_saveBuffer ??= new BufferWriter(true, World.SerializedTypes);
|
||||
_saveBuffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
void Serialize()
|
||||
{
|
||||
saveBuffer ??= new BufferWriter(true, World.SerializedTypes);
|
||||
saveBuffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
serializer(saveBuffer);
|
||||
}
|
||||
|
||||
void WriteSnapshot(string savePath)
|
||||
{
|
||||
string binPath = Path.Combine(savePath, name, $"{name}.bin");
|
||||
var buffer = saveBuffer!.Buffer.AsSpan(0, (int)saveBuffer.Position);
|
||||
AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer);
|
||||
}
|
||||
|
||||
void Deserialize(string savePath, Dictionary<ulong, string> typesDb) =>
|
||||
AdhocPersistence.Deserialize(Path.Combine(savePath, name, $"{name}.bin"), deserializer);
|
||||
|
||||
Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, priority);
|
||||
Serialize(_saveBuffer);
|
||||
}
|
||||
|
||||
public abstract void Serialize(IGenericWriter writer);
|
||||
|
||||
public override void WriteSnapshot(string basePath)
|
||||
{
|
||||
string binPath = Path.Combine(basePath, Name, $"{Name}.bin");
|
||||
var buffer = _saveBuffer!.Buffer.AsSpan(0, (int)_saveBuffer.Position);
|
||||
AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer);
|
||||
}
|
||||
|
||||
public override void Deserialize(string savePath, Dictionary<ulong, string> typesDb) =>
|
||||
AdhocPersistence.Deserialize(Path.Combine(savePath, Name, $"{Name}.bin"), Deserialize);
|
||||
|
||||
public abstract void Deserialize(IGenericReader reader);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,38 +21,31 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Server;
|
||||
|
||||
public static class Persistence
|
||||
public abstract class Persistence
|
||||
{
|
||||
public const int DefaultPriority = 100;
|
||||
private static readonly SortedSet<Persistence> _registry = new(new PersistenceComparer());
|
||||
|
||||
private static readonly SortedSet<RegistryEntry> _registry = new(new RegistryEntryComparer());
|
||||
public int Priority { get; }
|
||||
|
||||
public static void Register(
|
||||
string name,
|
||||
Action serializer,
|
||||
Action<string> snapshotWriter,
|
||||
Action<string, Dictionary<ulong, string>> deserializer,
|
||||
int priority = DefaultPriority
|
||||
)
|
||||
public Persistence(int priority = 100)
|
||||
{
|
||||
_registry.Add(
|
||||
new RegistryEntry
|
||||
{
|
||||
Name = name,
|
||||
Priority = priority,
|
||||
Serialize = serializer,
|
||||
WriteSnapshot = snapshotWriter,
|
||||
Deserialize = deserializer
|
||||
}
|
||||
);
|
||||
Priority = priority;
|
||||
_registry.Add(this);
|
||||
}
|
||||
|
||||
public static void Unregister(string name) => _registry.RemoveWhere(entry => entry.Name == name);
|
||||
public bool Register() => _registry.Add(this);
|
||||
|
||||
public void Unregister() => _registry.Remove(this);
|
||||
|
||||
public static void Load(string path)
|
||||
{
|
||||
var typesDb = LoadTypes(path);
|
||||
|
||||
foreach (var entry in _registry)
|
||||
{
|
||||
(entry as IGenericEntityPersistence)?.DeserializeIndexes(path, typesDb);
|
||||
}
|
||||
|
||||
// This should probably not be parallel since Mobiles must be loaded before Items
|
||||
foreach (var entry in _registry)
|
||||
{
|
||||
|
|
@ -86,9 +79,26 @@ public static class Persistence
|
|||
return db;
|
||||
}
|
||||
|
||||
public static void Serialize()
|
||||
internal static void SerializeAll()
|
||||
{
|
||||
Parallel.ForEach(_registry, entry => entry.Serialize());
|
||||
// TODO: Hand off to a scheduler
|
||||
Parallel.ForEach(_registry, p => p.Serialize());
|
||||
}
|
||||
|
||||
internal static void PostSerializeAll()
|
||||
{
|
||||
foreach (var p in _registry)
|
||||
{
|
||||
p.PostSerialize();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void PostDeserializeAll()
|
||||
{
|
||||
foreach (var p in _registry)
|
||||
{
|
||||
p.PostDeserialize();
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteSnapshot(string path, ConcurrentQueue<Type> types)
|
||||
|
|
@ -126,20 +136,24 @@ public static class Persistence
|
|||
}
|
||||
}
|
||||
|
||||
public record RegistryEntry
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public int Priority { get; init; }
|
||||
// Serializes to memory buffers and run in parallel
|
||||
public abstract void Serialize();
|
||||
|
||||
// Serializes to memory buffers and run in parallel
|
||||
public Action Serialize { get; init; }
|
||||
public Action<string> WriteSnapshot { get; init; }
|
||||
public Action<string, Dictionary<ulong, string>> Deserialize { get; init; }
|
||||
public abstract void WriteSnapshot(string savePath);
|
||||
|
||||
public abstract void Deserialize(string savePath, Dictionary<ulong, string> typesDb);
|
||||
|
||||
public virtual void PostSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
internal class RegistryEntryComparer : IComparer<RegistryEntry>
|
||||
public virtual void PostDeserialize()
|
||||
{
|
||||
public int Compare(RegistryEntry x, RegistryEntry y)
|
||||
}
|
||||
|
||||
internal class PersistenceComparer : IComparer<Persistence>
|
||||
{
|
||||
public int Compare(Persistence x, Persistence y)
|
||||
{
|
||||
if (x == y)
|
||||
{
|
||||
|
|
@ -160,7 +174,7 @@ public static class Persistence
|
|||
var cmp = x.Priority.CompareTo(y.Priority);
|
||||
|
||||
// Then alphabetically. We won't allow the same entry (by name) twice in the SortedSet
|
||||
return cmp != 0 ? cmp : x.Name?.CompareOrdinal(y.Name) ?? -1;
|
||||
return cmp != 0 ? cmp : x.GetHashCode().CompareTo(y.GetHashCode());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Server;
|
||||
|
||||
|
|
@ -72,12 +70,6 @@ public static class EntityPersistence
|
|||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SaveEntities<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,
|
||||
|
|
@ -176,6 +168,7 @@ public static class EntityPersistence
|
|||
}
|
||||
|
||||
idxReader.Close();
|
||||
entities.TrimExcess();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Server.Guilds;
|
||||
using Server.Logging;
|
||||
|
|
@ -41,120 +38,34 @@ public static class World
|
|||
{
|
||||
private static ILogger logger = LogFactory.GetLogger(typeof(World));
|
||||
|
||||
private static ItemPersistence _itemPersistence = new();
|
||||
private static MobilePersistence _mobilePersistence = new();
|
||||
private static GenericEntityPersistence<BaseGuild> _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF);
|
||||
|
||||
private static ManualResetEvent m_DiskWriteHandle = new(true);
|
||||
private static Dictionary<Serial, IEntity> _pendingAdd = new();
|
||||
private static Dictionary<Serial, IEntity> _pendingDelete = new();
|
||||
private static ConcurrentQueue<Item> _decayQueue = new();
|
||||
|
||||
private static string _tempSavePath; // Path to the temporary folder for the save
|
||||
private static bool _enableSaveStats;
|
||||
|
||||
public const bool DirtyTrackingEnabled = false;
|
||||
public const uint ItemOffset = 0x40000000;
|
||||
public const uint MaxItemSerial = 0x7FFFFFFF;
|
||||
public const uint MaxMobileSerial = ItemOffset - 1;
|
||||
private const uint _maxItems = MaxItemSerial - ItemOffset + 1;
|
||||
|
||||
private static Serial _lastMobile = Serial.Zero;
|
||||
private static Serial _lastItem = (Serial)ItemOffset;
|
||||
private static Serial _lastGuild = Serial.Zero;
|
||||
public static Serial NewMobile => _mobilePersistence.NewEntity;
|
||||
public static Serial NewItem => _itemPersistence.NewEntity;
|
||||
public static Serial NewGuild => _guildPersistence.NewEntity;
|
||||
|
||||
public static Serial NewMobile
|
||||
{
|
||||
get
|
||||
{
|
||||
#if THREADGUARD
|
||||
if (Thread.CurrentThread != Core.Thread)
|
||||
{
|
||||
logger.Error(
|
||||
"Attempted to get a new mobile serial from the wrong thread!\n{StackTrace}",
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
#endif
|
||||
var last = _lastMobile;
|
||||
var maxMobile = (Serial)MaxMobileSerial;
|
||||
|
||||
for (int i = 0; i < MaxMobileSerial; i++)
|
||||
{
|
||||
last++;
|
||||
|
||||
if (last > maxMobile)
|
||||
{
|
||||
last = (Serial)1;
|
||||
}
|
||||
|
||||
if (FindMobile(last, true) == null)
|
||||
{
|
||||
return _lastMobile = last;
|
||||
}
|
||||
}
|
||||
|
||||
OutOfMemory("No serials left to allocate for mobiles");
|
||||
return Serial.MinusOne;
|
||||
}
|
||||
}
|
||||
|
||||
public static Serial NewItem
|
||||
{
|
||||
get
|
||||
{
|
||||
#if THREADGUARD
|
||||
if (Thread.CurrentThread != Core.Thread)
|
||||
{
|
||||
logger.Error(
|
||||
"Attempted to get a new item serial from the wrong thread!\n{StackTrace}",
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
#endif
|
||||
var last = _lastItem;
|
||||
|
||||
for (int i = 0; i < _maxItems; i++)
|
||||
{
|
||||
last++;
|
||||
|
||||
if (last > MaxItemSerial)
|
||||
{
|
||||
last = (Serial)ItemOffset;
|
||||
}
|
||||
|
||||
if (FindItem(last, true) == null)
|
||||
{
|
||||
return _lastItem = last;
|
||||
}
|
||||
}
|
||||
|
||||
OutOfMemory("No serials left to allocate for items");
|
||||
return Serial.MinusOne;
|
||||
}
|
||||
}
|
||||
|
||||
public static Serial NewGuild
|
||||
{
|
||||
get
|
||||
{
|
||||
while (FindGuild(_lastGuild += 1) != null)
|
||||
{
|
||||
}
|
||||
|
||||
return _lastGuild;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OutOfMemory(string message) => throw new OutOfMemoryException(message);
|
||||
public static Dictionary<Serial, Item> Items => _itemPersistence.EntitiesBySerial;
|
||||
public static Dictionary<Serial, Mobile> Mobiles => _mobilePersistence.EntitiesBySerial;
|
||||
public static Dictionary<Serial, BaseGuild> Guilds => _guildPersistence.EntitiesBySerial;
|
||||
|
||||
public static string SavePath { get; private set; }
|
||||
|
||||
public static WorldState WorldState { get; private set; }
|
||||
public static bool Saving => WorldState == WorldState.Saving;
|
||||
public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial;
|
||||
public static bool Loading => WorldState == WorldState.Loading;
|
||||
|
||||
public static Dictionary<Serial, Mobile> Mobiles { get; private set; }
|
||||
public static Dictionary<Serial, Item> Items { get; private set; }
|
||||
public static Dictionary<Serial, BaseGuild> Guilds { get; private set; }
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
var tempSavePath = ServerConfiguration.GetSetting("world.tempSavePath", "temp");
|
||||
|
|
@ -162,11 +73,6 @@ public static class World
|
|||
|
||||
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
|
||||
SavePath = PathUtility.GetFullPath(savePath);
|
||||
|
||||
_enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false);
|
||||
|
||||
// Mobiles & Items
|
||||
Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -243,36 +149,6 @@ public static class World
|
|||
NetState.FlushAll();
|
||||
}
|
||||
|
||||
internal static void LoadEntities(string basePath, Dictionary<ulong, string> typesDb)
|
||||
{
|
||||
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
|
||||
|
||||
Mobiles = EntityPersistence.LoadIndex(basePath, mobileIndexInfo, typesDb, out List<EntitySpan<Mobile>> mobiles);
|
||||
Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, typesDb, out List<EntitySpan<Item>> items);
|
||||
Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, typesDb, out List<EntitySpan<BaseGuild>> guilds);
|
||||
|
||||
if (Mobiles.Count > 0)
|
||||
{
|
||||
_lastMobile = Mobiles.Keys.Max();
|
||||
}
|
||||
|
||||
if (Items.Count > 0)
|
||||
{
|
||||
_lastItem = Items.Keys.Max();
|
||||
}
|
||||
|
||||
if (Guilds.Count > 0)
|
||||
{
|
||||
_lastGuild = Guilds.Keys.Max();
|
||||
}
|
||||
|
||||
EntityPersistence.LoadData(basePath, mobileIndexInfo, typesDb, mobiles);
|
||||
EntityPersistence.LoadData(basePath, itemIndexInfo, typesDb, items);
|
||||
EntityPersistence.LoadData(basePath, guildIndexInfo, typesDb, guilds);
|
||||
}
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
if (WorldState != WorldState.Initial)
|
||||
|
|
@ -291,25 +167,7 @@ public static class World
|
|||
// Set the world to running before we process our queues
|
||||
WorldState = WorldState.Running;
|
||||
|
||||
ProcessSafetyQueues();
|
||||
|
||||
foreach (var item in Items.Values)
|
||||
{
|
||||
if (item.Parent == null)
|
||||
{
|
||||
item.UpdateTotals();
|
||||
}
|
||||
|
||||
item.ClearProperties();
|
||||
}
|
||||
|
||||
foreach (var m in Mobiles.Values)
|
||||
{
|
||||
m.UpdateRegion(); // Is this really needed?
|
||||
m.UpdateTotals();
|
||||
|
||||
m.ClearProperties();
|
||||
}
|
||||
Persistence.PostDeserializeAll(); // Process safety queues
|
||||
|
||||
watch.Stop();
|
||||
|
||||
|
|
@ -321,102 +179,13 @@ public static class World
|
|||
);
|
||||
}
|
||||
|
||||
private static void ProcessSafetyQueues()
|
||||
{
|
||||
foreach (var entity in _pendingAdd.Values)
|
||||
{
|
||||
AddEntity(entity);
|
||||
}
|
||||
|
||||
_pendingAdd.Clear();
|
||||
|
||||
foreach (var entity in _pendingDelete.Values)
|
||||
{
|
||||
if (_pendingAdd.ContainsKey(entity.Serial))
|
||||
{
|
||||
logger.Warning("Entity {Entity} was both pending deletion and addition after save", entity);
|
||||
}
|
||||
|
||||
RemoveEntity(entity);
|
||||
}
|
||||
|
||||
_pendingDelete.Clear();
|
||||
}
|
||||
|
||||
private static void AppendSafetyLog(string action, ISerializable entity)
|
||||
{
|
||||
var message =
|
||||
$"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected.";
|
||||
|
||||
logger.Information(message, action, entity);
|
||||
|
||||
try
|
||||
{
|
||||
using var op = new StreamWriter("world-save-errors.log", true);
|
||||
op.WriteLine("{0}\t{1}", DateTime.UtcNow, message);
|
||||
op.WriteLine(new StackTrace(2).ToString());
|
||||
op.WriteLine();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private static void FinishWorldSave()
|
||||
{
|
||||
WorldState = WorldState.Running;
|
||||
|
||||
ProcessDecay();
|
||||
ProcessSafetyQueues();
|
||||
}
|
||||
|
||||
private static void TraceSave(params IEnumerable<KeyValuePair<string, int>>[] entityTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
var timestamp = Utility.GetTimeStamp();
|
||||
var saveStatsPath = Path.Combine(Core.BaseDirectory, $"Logs/Saves/Save-Stats-{timestamp}.log");
|
||||
PathUtility.EnsureDirectory(saveStatsPath);
|
||||
|
||||
using var op = new StreamWriter(saveStatsPath, true);
|
||||
|
||||
for (var i = 0; i < entityTypes.Length; i++)
|
||||
{
|
||||
foreach (var (t, c) in entityTypes[i])
|
||||
{
|
||||
op.WriteLine("{0}: {1}", t, c);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
op.WriteLine("- Total: {0}", count);
|
||||
|
||||
op.WriteLine();
|
||||
op.WriteLine();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteEntities(string basePath)
|
||||
{
|
||||
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
|
||||
|
||||
EntityPersistence.WriteEntities(mobileIndexInfo, Mobiles, basePath, SerializedTypes, out var mobileCounts);
|
||||
EntityPersistence.WriteEntities(itemIndexInfo, Items, basePath, SerializedTypes, out var itemCounts);
|
||||
EntityPersistence.WriteEntities(guildIndexInfo, Guilds, basePath, SerializedTypes, out var guildCounts);
|
||||
|
||||
if (_enableSaveStats)
|
||||
{
|
||||
TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList());
|
||||
}
|
||||
Persistence.PostSerializeAll(); // Process safety queues
|
||||
}
|
||||
|
||||
public static void WriteFiles(object state)
|
||||
|
|
@ -516,25 +285,6 @@ public static class World
|
|||
*/
|
||||
public static ConcurrentQueue<Type> SerializedTypes { get; } = new();
|
||||
|
||||
private static void SaveEntities()
|
||||
{
|
||||
_serializationStart = DateTime.UtcNow;
|
||||
EntityPersistence.SaveEntities(Items.Values, SaveEntity);
|
||||
EntityPersistence.SaveEntities(Mobiles.Values, SaveEntity);
|
||||
EntityPersistence.SaveEntities(Guilds.Values, SaveEntity);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void SaveEntity<T>(T entity) where T : class, ISerializable
|
||||
{
|
||||
if (entity is Item item && item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart)
|
||||
{
|
||||
EnqueueForDecay(item);
|
||||
}
|
||||
|
||||
entity.Serialize(SerializedTypes);
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
if (WorldState != WorldState.Running)
|
||||
|
|
@ -558,7 +308,8 @@ public static class World
|
|||
|
||||
try
|
||||
{
|
||||
Persistence.Serialize();
|
||||
_serializationStart = Core.Now;
|
||||
Persistence.SerializeAll();
|
||||
EventSink.InvokeWorldSave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -593,231 +344,119 @@ public static class World
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = true) =>
|
||||
FindEntity<IEntity>(serial, returnDeleted, returnPending);
|
||||
|
||||
public static T FindEntity<T>(Serial serial, bool returnDeleted = false, bool returnPending = true) where T : class, IEntity
|
||||
{
|
||||
switch (WorldState)
|
||||
{
|
||||
default: return default;
|
||||
case WorldState.Loading:
|
||||
case WorldState.Saving:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity))
|
||||
{
|
||||
return entity as T;
|
||||
}
|
||||
|
||||
if (!returnPending || !_pendingAdd.TryGetValue(serial, out entity))
|
||||
{
|
||||
if (serial.IsItem)
|
||||
{
|
||||
if (Items.TryGetValue(serial, out var item))
|
||||
{
|
||||
return item as T;
|
||||
}
|
||||
}
|
||||
else // if (serial.IsMobile)
|
||||
{
|
||||
if (Mobiles.TryGetValue(serial, out var mob))
|
||||
{
|
||||
return mob as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
if (serial.IsItem)
|
||||
{
|
||||
return Items.TryGetValue(serial, out var item) ? item as T : null;
|
||||
}
|
||||
|
||||
if (serial.IsMobile)
|
||||
{
|
||||
return Mobiles.TryGetValue(serial, out var mob) ? mob as T : null;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Item FindItem(Serial serial, bool returnDeleted = false) => FindEntity<Item>(serial, returnDeleted);
|
||||
public static Item FindItem(Serial serial, bool returnDeleted = false) => _itemPersistence.Find(serial, returnDeleted);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Mobile FindMobile(Serial serial, bool returnDeleted = false) =>
|
||||
FindEntity<Mobile>(serial, returnDeleted);
|
||||
_mobilePersistence.Find(serial, returnDeleted);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BaseGuild FindGuild(Serial serial) => Guilds.TryGetValue(serial, out var guild) ? guild : null;
|
||||
|
||||
public static void AddEntity<T>(T entity) where T : class, IEntity
|
||||
// Legacy: Only used for retrieving Items and Mobiles.
|
||||
public static void AddEntity(IEntity entity)
|
||||
{
|
||||
switch (WorldState)
|
||||
if (entity is Item item)
|
||||
{
|
||||
default: // Not Running
|
||||
{
|
||||
throw new Exception($"Added {entity.GetType().Name} before world load.");
|
||||
}
|
||||
case WorldState.Saving:
|
||||
{
|
||||
AppendSafetyLog("add", entity);
|
||||
goto case WorldState.WritingSave;
|
||||
}
|
||||
case WorldState.Loading:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
if (_pendingDelete.Remove(entity.Serial))
|
||||
{
|
||||
logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, WorldState.ToString());
|
||||
}
|
||||
_pendingAdd[entity.Serial] = entity;
|
||||
break;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
if (entity.Serial.IsItem)
|
||||
{
|
||||
ref var item = ref CollectionsMarshal.GetValueRefOrAddDefault(Items, entity.Serial, out bool exists);
|
||||
if (exists)
|
||||
{
|
||||
if (item == entity)
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
item.GetType().FullName,
|
||||
item.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item = entity as Item;
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.Serial.IsMobile)
|
||||
{
|
||||
ref var mob = ref CollectionsMarshal.GetValueRefOrAddDefault(Mobiles, entity.Serial, out bool exists);
|
||||
if (exists)
|
||||
{
|
||||
if (mob == entity)
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Mobiles but it already exists in the collection.{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Mobiles but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
mob.GetType().FullName,
|
||||
mob.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mob = entity as Mobile;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
_itemPersistence.AddEntity(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddGuild(BaseGuild entity)
|
||||
{
|
||||
ref var guild = ref CollectionsMarshal.GetValueRefOrAddDefault(Guilds, entity.Serial, out bool exists);
|
||||
if (exists)
|
||||
else if (entity is Mobile mobile)
|
||||
{
|
||||
if (guild == entity)
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Guilds but it already exists in the collection.{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(
|
||||
$"Attempted to add '{{Entity}}' ({{Serial}}) to World.Guilds but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}",
|
||||
entity.GetType().FullName,
|
||||
entity.Serial,
|
||||
guild.GetType().FullName,
|
||||
guild.Serial,
|
||||
new StackTrace()
|
||||
);
|
||||
}
|
||||
_mobilePersistence.AddEntity(mobile);
|
||||
}
|
||||
else
|
||||
{
|
||||
guild = entity;
|
||||
logger.Warning($"Attempted to call World.AddEntity with '{entity.GetType()}'. Must be a mobile or item.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveEntity<T>(T entity) where T : class, IEntity
|
||||
public static void RemoveEntity(IEntity entity)
|
||||
{
|
||||
switch (WorldState)
|
||||
if (entity is Item item)
|
||||
{
|
||||
default: // Not Running
|
||||
{
|
||||
throw new Exception($"Removed {entity.GetType().Name} before world load.");
|
||||
}
|
||||
case WorldState.Saving:
|
||||
{
|
||||
AppendSafetyLog("delete", entity);
|
||||
goto case WorldState.WritingSave;
|
||||
}
|
||||
case WorldState.Loading:
|
||||
case WorldState.WritingSave:
|
||||
{
|
||||
_pendingAdd.Remove(entity.Serial);
|
||||
_pendingDelete[entity.Serial] = entity;
|
||||
break;
|
||||
}
|
||||
case WorldState.Running:
|
||||
{
|
||||
if (entity.Serial.IsItem)
|
||||
{
|
||||
Items.Remove(entity.Serial);
|
||||
}
|
||||
|
||||
if (entity.Serial.IsMobile)
|
||||
{
|
||||
Mobiles.Remove(entity.Serial);
|
||||
}
|
||||
break;
|
||||
}
|
||||
_itemPersistence.RemoveEntity(item);
|
||||
}
|
||||
else if (entity is Mobile mobile)
|
||||
{
|
||||
_mobilePersistence.RemoveEntity(mobile);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warning($"Attempted to call World.RemoveEntity with '{entity.GetType()}'. Must be a mobile or item.");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void RemoveGuild(BaseGuild guild) => Guilds.Remove(guild.Serial);
|
||||
public static BaseGuild FindGuild(Serial serial) => _guildPersistence.Find(serial);
|
||||
|
||||
public static void AddGuild(BaseGuild guild) => _guildPersistence.AddEntity(guild);
|
||||
|
||||
public static void RemoveGuild(BaseGuild guild) => _guildPersistence.RemoveEntity(guild);
|
||||
|
||||
// Legacy: Only used for retrieving Items and Mobiles.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = false) =>
|
||||
FindEntity<IEntity>(serial, returnDeleted, returnPending);
|
||||
|
||||
// Legacy: Only used for retrieving Items and Mobiles.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T FindEntity<T>(Serial serial, bool returnDeleted = false, bool returnPending = false)
|
||||
where T : class, IEntity
|
||||
{
|
||||
if (serial.IsItem)
|
||||
{
|
||||
return _itemPersistence.Find(serial, returnDeleted, returnPending) as T;
|
||||
}
|
||||
|
||||
return _mobilePersistence.Find(serial, returnDeleted, returnPending) as T;
|
||||
}
|
||||
|
||||
private class ItemPersistence : GenericEntityPersistence<Item>
|
||||
{
|
||||
public ItemPersistence() : base("Items", 2, ItemOffset, MaxItemSerial)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void SerializeEntity(Item item)
|
||||
{
|
||||
if (item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart)
|
||||
{
|
||||
EnqueueForDecay(item);
|
||||
}
|
||||
|
||||
((ISerializable)item).Serialize(SerializedTypes);
|
||||
}
|
||||
|
||||
public override void PostDeserialize()
|
||||
{
|
||||
base.PostDeserialize();
|
||||
|
||||
foreach (var item in EntitiesBySerial.Values)
|
||||
{
|
||||
if (item.Parent == null)
|
||||
{
|
||||
item.UpdateTotals();
|
||||
}
|
||||
|
||||
item.ClearProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MobilePersistence : GenericEntityPersistence<Mobile>
|
||||
{
|
||||
public MobilePersistence() : base("Mobiles", 1, 1, MaxMobileSerial)
|
||||
{
|
||||
}
|
||||
|
||||
public override void PostDeserialize()
|
||||
{
|
||||
base.PostDeserialize();
|
||||
|
||||
foreach (var m in EntitiesBySerial.Values)
|
||||
{
|
||||
m.UpdateRegion(); // Is this really needed?
|
||||
m.UpdateTotals();
|
||||
|
||||
m.ClearProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue