fix: Adds generic entity persistence support and BOBEntry as entities (#1527)
### Summary
Adds a generic entity persistence. This can be used to create new entity types that have a `Serial`.
Here is an example:
```cs
public class BOBEntries : GenericEntitySerialization<IBOBEntry>
{
public static void Configure()
{
Configure("BOBEntries");
}
}
```
The annotation tells the system what folder to serialize the entries to. The class/interface (`IBOBEntry`) is the root type that implements `ISerializable`.
This commit is contained in:
parent
666b83a3dd
commit
e0225c6e59
22 changed files with 666 additions and 311 deletions
209
Projects/Server/Serialization/GenericEntitySerialization.cs
Normal file
209
Projects/Server/Serialization/GenericEntitySerialization.cs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -24,8 +24,6 @@ public interface ISerializable
|
|||
// Should be serialized/deserialized with the index so it can be referenced by IGenericReader
|
||||
DateTime Created { get; set; }
|
||||
|
||||
// Should be serialized/deserialized with the index so it can be referenced by IGenericReader
|
||||
DateTime LastSerialized { get; protected internal set; }
|
||||
long SavePosition { get; protected internal set; }
|
||||
BufferWriter SaveBuffer { get; protected internal set; }
|
||||
|
||||
|
|
@ -61,7 +59,6 @@ public interface ISerializable
|
|||
return;
|
||||
}
|
||||
|
||||
LastSerialized = Core.Now;
|
||||
SaveBuffer.Seek(0, SeekOrigin.Begin);
|
||||
Serialize(SaveBuffer);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ namespace Server;
|
|||
|
||||
public static class SerializationExtensions
|
||||
{
|
||||
private static readonly Dictionary<Type, Func<Serial, bool, bool, ISerializable>> _directFinderTable = new();
|
||||
private static readonly Dictionary<Type, Func<Serial, bool, bool, ISerializable>> _searchTable = new();
|
||||
|
||||
public static void RegisterFindEntity(this Type type, Func<Serial, bool, bool, ISerializable> func)
|
||||
{
|
||||
_searchTable[type] = func;
|
||||
}
|
||||
|
||||
public static T ReadEntity<T>(this IGenericReader reader) where T : class, ISerializable
|
||||
{
|
||||
Serial serial = reader.ReadSerial();
|
||||
|
|
@ -31,21 +39,55 @@ public static class SerializationExtensions
|
|||
// Add to this list when creating new serializable types
|
||||
if (typeof(BaseGuild).IsAssignableFrom(typeT))
|
||||
{
|
||||
entity = World.FindGuild(serial) as T;
|
||||
// If we check for `entity.Deleted` here during deserialization then all guilds are deleted because
|
||||
// Deleted -> Disbanded -> No leader, which is the case before deserialization.
|
||||
// TODO: Use a deleted flag instead, and actively check for dibanded guilds properly.
|
||||
// TODO: Use a deleted flag instead, and actively check for disbanded guilds properly.
|
||||
return World.FindGuild(serial) as T;
|
||||
}
|
||||
else
|
||||
|
||||
if (typeof(IEntity).IsAssignableFrom(typeT))
|
||||
{
|
||||
entity = World.FindEntity<IEntity>(serial) as T;
|
||||
if (entity?.Deleted == false)
|
||||
return World.FindEntity<IEntity>(serial, returnPending: false) as T;
|
||||
}
|
||||
|
||||
if (_directFinderTable.TryGetValue(typeT, out var finder))
|
||||
{
|
||||
return finder(serial, false, false) as T;
|
||||
}
|
||||
|
||||
Type type = null;
|
||||
foreach (var baseType in _searchTable.Keys)
|
||||
{
|
||||
if (baseType.IsAssignableFrom(typeT))
|
||||
{
|
||||
return entity;
|
||||
type = baseType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entity?.Created <= reader.LastSerialized ? entity : null;
|
||||
if (type == null)
|
||||
{
|
||||
type = typeT;
|
||||
while (true)
|
||||
{
|
||||
var baseType = type?.BaseType;
|
||||
|
||||
// Find the parent class with ISerializable registered. To do this we break on it's parent class (or object)
|
||||
// that doesn't have ISerializable implemented.
|
||||
if (baseType?.GetInterface("ISerializable") == null && type?.GetInterface("ISerializable") != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
type = baseType;
|
||||
}
|
||||
|
||||
throw new Exception($"No FindEntity registered for '{type.FullName}'.");
|
||||
}
|
||||
|
||||
finder = _searchTable[type];
|
||||
_directFinderTable[type] = finder;
|
||||
return finder(serial, false, false) as T;
|
||||
}
|
||||
|
||||
public static List<T> ReadEntityList<T>(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue