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.
|
* 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)
|
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;
|
namespace Server;
|
||||||
|
|
||||||
public static class GenericPersistence
|
public abstract class GenericPersistence : Persistence
|
||||||
{
|
{
|
||||||
public static void Register(
|
private BufferWriter _saveBuffer;
|
||||||
string name,
|
|
||||||
Action<IGenericWriter> serializer,
|
public string Name { get; }
|
||||||
Action<IGenericReader> deserializer,
|
|
||||||
int priority = Persistence.DefaultPriority
|
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()
|
Serialize(_saveBuffer);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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(
|
public Persistence(int priority = 100)
|
||||||
string name,
|
|
||||||
Action serializer,
|
|
||||||
Action<string> snapshotWriter,
|
|
||||||
Action<string, Dictionary<ulong, string>> deserializer,
|
|
||||||
int priority = DefaultPriority
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
_registry.Add(
|
Priority = priority;
|
||||||
new RegistryEntry
|
_registry.Add(this);
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Priority = priority,
|
|
||||||
Serialize = serializer,
|
|
||||||
WriteSnapshot = snapshotWriter,
|
|
||||||
Deserialize = deserializer
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
public static void Load(string path)
|
||||||
{
|
{
|
||||||
var typesDb = LoadTypes(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
|
// This should probably not be parallel since Mobiles must be loaded before Items
|
||||||
foreach (var entry in _registry)
|
foreach (var entry in _registry)
|
||||||
{
|
{
|
||||||
|
|
@ -86,9 +79,26 @@ public static class Persistence
|
||||||
return db;
|
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)
|
public static void WriteSnapshot(string path, ConcurrentQueue<Type> types)
|
||||||
|
|
@ -126,20 +136,24 @@ public static class Persistence
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record RegistryEntry
|
// Serializes to memory buffers and run in parallel
|
||||||
{
|
public abstract void Serialize();
|
||||||
public string Name { get; init; }
|
|
||||||
public int Priority { get; init; }
|
|
||||||
|
|
||||||
// Serializes to memory buffers and run in parallel
|
public abstract void WriteSnapshot(string savePath);
|
||||||
public Action Serialize { get; init; }
|
|
||||||
public Action<string> WriteSnapshot { get; init; }
|
public abstract void Deserialize(string savePath, Dictionary<ulong, string> typesDb);
|
||||||
public Action<string, Dictionary<ulong, string>> Deserialize { get; init; }
|
|
||||||
|
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)
|
if (x == y)
|
||||||
{
|
{
|
||||||
|
|
@ -160,7 +174,7 @@ public static class Persistence
|
||||||
var cmp = x.Priority.CompareTo(y.Priority);
|
var cmp = x.Priority.CompareTo(y.Priority);
|
||||||
|
|
||||||
// Then alphabetically. We won't allow the same entry (by name) twice in the SortedSet
|
// 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;
|
||||||
using System.IO.MemoryMappedFiles;
|
using System.IO.MemoryMappedFiles;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Server;
|
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>(
|
public static Dictionary<I, T> LoadIndex<I, T>(
|
||||||
string path,
|
string path,
|
||||||
IIndexInfo<I> indexInfo,
|
IIndexInfo<I> indexInfo,
|
||||||
|
|
@ -176,6 +168,7 @@ public static class EntityPersistence
|
||||||
}
|
}
|
||||||
|
|
||||||
idxReader.Close();
|
idxReader.Close();
|
||||||
|
entities.TrimExcess();
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@ using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Server.Guilds;
|
using Server.Guilds;
|
||||||
using Server.Logging;
|
using Server.Logging;
|
||||||
|
|
@ -41,120 +38,34 @@ public static class World
|
||||||
{
|
{
|
||||||
private static ILogger logger = LogFactory.GetLogger(typeof(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 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 ConcurrentQueue<Item> _decayQueue = new();
|
||||||
|
|
||||||
private static string _tempSavePath; // Path to the temporary folder for the save
|
private static string _tempSavePath; // Path to the temporary folder for the save
|
||||||
private static bool _enableSaveStats;
|
|
||||||
|
|
||||||
public const bool DirtyTrackingEnabled = false;
|
public const bool DirtyTrackingEnabled = false;
|
||||||
public const uint ItemOffset = 0x40000000;
|
public const uint ItemOffset = 0x40000000;
|
||||||
public const uint MaxItemSerial = 0x7FFFFFFF;
|
public const uint MaxItemSerial = 0x7FFFFFFF;
|
||||||
public const uint MaxMobileSerial = ItemOffset - 1;
|
public const uint MaxMobileSerial = ItemOffset - 1;
|
||||||
private const uint _maxItems = MaxItemSerial - ItemOffset + 1;
|
|
||||||
|
|
||||||
private static Serial _lastMobile = Serial.Zero;
|
public static Serial NewMobile => _mobilePersistence.NewEntity;
|
||||||
private static Serial _lastItem = (Serial)ItemOffset;
|
public static Serial NewItem => _itemPersistence.NewEntity;
|
||||||
private static Serial _lastGuild = Serial.Zero;
|
public static Serial NewGuild => _guildPersistence.NewEntity;
|
||||||
|
|
||||||
public static Serial NewMobile
|
public static Dictionary<Serial, Item> Items => _itemPersistence.EntitiesBySerial;
|
||||||
{
|
public static Dictionary<Serial, Mobile> Mobiles => _mobilePersistence.EntitiesBySerial;
|
||||||
get
|
public static Dictionary<Serial, BaseGuild> Guilds => _guildPersistence.EntitiesBySerial;
|
||||||
{
|
|
||||||
#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 string SavePath { get; private set; }
|
public static string SavePath { get; private set; }
|
||||||
|
|
||||||
public static WorldState WorldState { get; private set; }
|
public static WorldState WorldState { get; private set; }
|
||||||
public static bool Saving => WorldState == WorldState.Saving;
|
public static bool Saving => WorldState == WorldState.Saving;
|
||||||
public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial;
|
public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial;
|
||||||
public static bool Loading => WorldState == WorldState.Loading;
|
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()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
var tempSavePath = ServerConfiguration.GetSetting("world.tempSavePath", "temp");
|
var tempSavePath = ServerConfiguration.GetSetting("world.tempSavePath", "temp");
|
||||||
|
|
@ -162,11 +73,6 @@ public static class World
|
||||||
|
|
||||||
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
|
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
|
||||||
SavePath = PathUtility.GetFullPath(savePath);
|
SavePath = PathUtility.GetFullPath(savePath);
|
||||||
|
|
||||||
_enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false);
|
|
||||||
|
|
||||||
// Mobiles & Items
|
|
||||||
Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -243,36 +149,6 @@ public static class World
|
||||||
NetState.FlushAll();
|
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()
|
public static void Load()
|
||||||
{
|
{
|
||||||
if (WorldState != WorldState.Initial)
|
if (WorldState != WorldState.Initial)
|
||||||
|
|
@ -291,25 +167,7 @@ public static class World
|
||||||
// Set the world to running before we process our queues
|
// Set the world to running before we process our queues
|
||||||
WorldState = WorldState.Running;
|
WorldState = WorldState.Running;
|
||||||
|
|
||||||
ProcessSafetyQueues();
|
Persistence.PostDeserializeAll(); // Process safety queues
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch.Stop();
|
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()
|
private static void FinishWorldSave()
|
||||||
{
|
{
|
||||||
WorldState = WorldState.Running;
|
WorldState = WorldState.Running;
|
||||||
|
|
||||||
ProcessDecay();
|
ProcessDecay();
|
||||||
ProcessSafetyQueues();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceSave(params IEnumerable<KeyValuePair<string, int>>[] entityTypes)
|
Persistence.PostSerializeAll(); // Process safety queues
|
||||||
{
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void WriteFiles(object state)
|
public static void WriteFiles(object state)
|
||||||
|
|
@ -516,25 +285,6 @@ public static class World
|
||||||
*/
|
*/
|
||||||
public static ConcurrentQueue<Type> SerializedTypes { get; } = new();
|
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()
|
public static void Save()
|
||||||
{
|
{
|
||||||
if (WorldState != WorldState.Running)
|
if (WorldState != WorldState.Running)
|
||||||
|
|
@ -558,7 +308,8 @@ public static class World
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Persistence.Serialize();
|
_serializationStart = Core.Now;
|
||||||
|
Persistence.SerializeAll();
|
||||||
EventSink.InvokeWorldSave();
|
EventSink.InvokeWorldSave();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -593,231 +344,119 @@ public static class World
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = true) =>
|
public static Item FindItem(Serial serial, bool returnDeleted = false) => _itemPersistence.Find(serial, returnDeleted);
|
||||||
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);
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Mobile FindMobile(Serial serial, bool returnDeleted = false) =>
|
public static Mobile FindMobile(Serial serial, bool returnDeleted = false) =>
|
||||||
FindEntity<Mobile>(serial, returnDeleted);
|
_mobilePersistence.Find(serial, returnDeleted);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
// Legacy: Only used for retrieving Items and Mobiles.
|
||||||
public static BaseGuild FindGuild(Serial serial) => Guilds.TryGetValue(serial, out var guild) ? guild : null;
|
public static void AddEntity(IEntity entity)
|
||||||
|
|
||||||
public static void AddEntity<T>(T entity) where T : class, IEntity
|
|
||||||
{
|
{
|
||||||
switch (WorldState)
|
if (entity is Item item)
|
||||||
{
|
{
|
||||||
default: // Not Running
|
_itemPersistence.AddEntity(item);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
else if (entity is Mobile mobile)
|
||||||
|
|
||||||
public static void AddGuild(BaseGuild entity)
|
|
||||||
{
|
|
||||||
ref var guild = ref CollectionsMarshal.GetValueRefOrAddDefault(Guilds, entity.Serial, out bool exists);
|
|
||||||
if (exists)
|
|
||||||
{
|
{
|
||||||
if (guild == entity)
|
_mobilePersistence.AddEntity(mobile);
|
||||||
{
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
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
|
_itemPersistence.RemoveEntity(item);
|
||||||
{
|
}
|
||||||
throw new Exception($"Removed {entity.GetType().Name} before world load.");
|
else if (entity is Mobile mobile)
|
||||||
}
|
{
|
||||||
case WorldState.Saving:
|
_mobilePersistence.RemoveEntity(mobile);
|
||||||
{
|
}
|
||||||
AppendSafetyLog("delete", entity);
|
else
|
||||||
goto case WorldState.WritingSave;
|
{
|
||||||
}
|
logger.Warning($"Attempted to call World.RemoveEntity with '{entity.GetType()}'. Must be a mobile or item.");
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,139 +1,95 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Server.Logging;
|
using Server.Logging;
|
||||||
|
|
||||||
namespace Server.Accounting
|
namespace Server.Accounting;
|
||||||
|
|
||||||
|
public class Accounts : GenericEntityPersistence<Account>
|
||||||
{
|
{
|
||||||
public static class Accounts
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Accounts));
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, Account> _accountsByName = new(32, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static int Count => _accountsByName.Count;
|
||||||
|
|
||||||
|
private static Accounts _accountsPersistence;
|
||||||
|
|
||||||
|
public static Serial NewAccount => _accountsPersistence.NewEntity;
|
||||||
|
|
||||||
|
public static void Configure()
|
||||||
{
|
{
|
||||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Accounts));
|
_accountsPersistence = new Accounts();
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly Dictionary<string, Account> _accountsByName = new(32, StringComparer.OrdinalIgnoreCase);
|
public Accounts() : base("Accounts", 3, 0x1, 0x7FFFFFFF)
|
||||||
private static Dictionary<Serial, Account> _accountsById = new(32);
|
{
|
||||||
private static Serial _lastAccount;
|
}
|
||||||
|
|
||||||
private static void OutOfMemory(string message) => throw new OutOfMemoryException(message);
|
public static IEnumerable<IAccount> GetAccounts() => _accountsByName.Values;
|
||||||
|
|
||||||
public static Serial NewAccount
|
public static Account GetAccount(string username)
|
||||||
|
{
|
||||||
|
_accountsByName.TryGetValue(username, out var a);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Add(Account a)
|
||||||
|
{
|
||||||
|
_accountsByName[a.Username] = a;
|
||||||
|
_accountsPersistence.AddEntity(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Remove(Account a)
|
||||||
|
{
|
||||||
|
_accountsByName.Remove(a.Username);
|
||||||
|
_accountsPersistence.RemoveEntity(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Deserialize(string path, Dictionary<ulong, string> typesDb)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(path, "Accounts", "accounts.xml");
|
||||||
|
|
||||||
|
// Backward Compatibility
|
||||||
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
get
|
DeserializeXml(filePath);
|
||||||
{
|
return;
|
||||||
var 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 => _accountsByName.Count;
|
base.Deserialize(path, typesDb);
|
||||||
|
|
||||||
public static void Configure() =>
|
foreach (var a in EntitiesBySerial.Values)
|
||||||
Persistence.Register("Accounts", Serialize, WriteSnapshot, Deserialize);
|
|
||||||
|
|
||||||
internal static void Serialize()
|
|
||||||
{
|
|
||||||
EntityPersistence.SaveEntities(
|
|
||||||
_accountsById.Values,
|
|
||||||
account => ((ISerializable)account).Serialize(World.SerializedTypes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void WriteSnapshot(string basePath)
|
|
||||||
{
|
|
||||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex("Accounts");
|
|
||||||
EntityPersistence.WriteEntities(indexInfo, _accountsById, basePath,World.SerializedTypes, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<IAccount> GetAccounts() => _accountsByName.Values;
|
|
||||||
|
|
||||||
public static Account GetAccount(string username)
|
|
||||||
{
|
|
||||||
_accountsByName.TryGetValue(username, out var a);
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Add(Account a)
|
|
||||||
{
|
{
|
||||||
_accountsByName[a.Username] = a;
|
_accountsByName[a.Username] = a;
|
||||||
_accountsById[a.Serial] = a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Remove(Account a)
|
|
||||||
{
|
|
||||||
_accountsByName.Remove(a.Username);
|
|
||||||
_accountsById.Remove(a.Serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Deserialize(string path, Dictionary<ulong, string> typesDb)
|
|
||||||
{
|
|
||||||
var filePath = Path.Combine(path, "Accounts", "accounts.xml");
|
|
||||||
|
|
||||||
// Backward Compatibility
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
{
|
|
||||||
DeserializeXml(filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IIndexInfo<Serial> indexInfo = new EntityTypeIndex("Accounts");
|
|
||||||
|
|
||||||
_accountsById = EntityPersistence.LoadIndex(path, indexInfo, typesDb, out List<EntitySpan<Account>> accounts);
|
|
||||||
|
|
||||||
if (_accountsById.Count > 0)
|
|
||||||
{
|
|
||||||
_lastAccount = _accountsById.Keys.Max();
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityPersistence.LoadData(path, indexInfo, typesDb, accounts);
|
|
||||||
|
|
||||||
foreach (var a in _accountsById.Values)
|
|
||||||
{
|
|
||||||
_accountsByName[a.Username] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DeserializeXml(string filePath)
|
|
||||||
{
|
|
||||||
var doc = new XmlDocument();
|
|
||||||
doc.Load(filePath);
|
|
||||||
|
|
||||||
var root = doc["accounts"];
|
|
||||||
|
|
||||||
if (root == null)
|
|
||||||
{
|
|
||||||
throw new FileLoadException("Unable to load xml file");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (XmlElement account in root.GetElementsByTagName("account"))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
new Account(account);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
logger.Warning("Account instance load failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IAccount FindAccount(Serial serial)
|
|
||||||
{
|
|
||||||
_accountsById.TryGetValue(serial, out var account);
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void DeserializeXml(string filePath)
|
||||||
|
{
|
||||||
|
var doc = new XmlDocument();
|
||||||
|
doc.Load(filePath);
|
||||||
|
|
||||||
|
var root = doc["accounts"];
|
||||||
|
|
||||||
|
if (root == null)
|
||||||
|
{
|
||||||
|
throw new FileLoadException("Unable to load xml file");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (XmlElement account in root.GetElementsByTagName("account"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new Account(account);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
logger.Warning("Account instance load failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IAccount FindAccount(Serial serial) => _accountsPersistence.FindEntity<Account>(serial);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
namespace Server.Engines.BulkOrders;
|
namespace Server.Engines.BulkOrders;
|
||||||
|
|
||||||
public class BOBEntries : GenericEntitySerialization<IBOBEntry>
|
public class BOBEntries : GenericEntityPersistence<IBOBEntry>
|
||||||
{
|
{
|
||||||
|
private static BOBEntries _bobEntriesPersistence;
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
Configure("BOBEntries");
|
_bobEntriesPersistence = new BOBEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BOBEntries() : base("BOBEntries", 3, 0x1, 0x7FFFFFFF)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Serial NewBOBEntry => _bobEntriesPersistence.NewEntity;
|
||||||
|
|
||||||
|
public static void Add(IBOBEntry entity) => _bobEntriesPersistence.AddEntity(entity);
|
||||||
|
|
||||||
|
public static void Remove(IBOBEntry entity) => _bobEntriesPersistence.AddEntity(entity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,14 @@ public abstract partial class BaseBOBEntry : IBOBEntry
|
||||||
|
|
||||||
public BaseBOBEntry()
|
public BaseBOBEntry()
|
||||||
{
|
{
|
||||||
Serial = BOBEntries.NewEntity;
|
Serial = BOBEntries.NewBOBEntry;
|
||||||
BOBEntries.AddEntity(this);
|
BOBEntries.Add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Delete()
|
public virtual void Delete()
|
||||||
{
|
{
|
||||||
Deleted = true;
|
Deleted = true;
|
||||||
BOBEntries.RemoveEntity(this);
|
BOBEntries.Remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Item Reconstruct();
|
public abstract Item Reconstruct();
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ namespace Server.Engines.BulkOrders
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
{
|
{
|
||||||
var largeEntry = new BOBLargeEntry(BOBEntries.NewEntity);
|
var largeEntry = new BOBLargeEntry(BOBEntries.NewBOBEntry);
|
||||||
largeEntry.Deserialize(reader);
|
largeEntry.Deserialize(reader);
|
||||||
|
|
||||||
Entries.Add(largeEntry);
|
Entries.Add(largeEntry);
|
||||||
|
|
@ -253,7 +253,7 @@ namespace Server.Engines.BulkOrders
|
||||||
}
|
}
|
||||||
case 1:
|
case 1:
|
||||||
{
|
{
|
||||||
var smallEntry = new BOBSmallEntry(BOBEntries.NewEntity);
|
var smallEntry = new BOBSmallEntry(BOBEntries.NewBOBEntry);
|
||||||
smallEntry.Deserialize(reader);
|
smallEntry.Deserialize(reader);
|
||||||
Entries.Add(smallEntry);
|
Entries.Add(smallEntry);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ using Server.Mobiles;
|
||||||
|
|
||||||
namespace Server.Engines.CannedEvil;
|
namespace Server.Engines.CannedEvil;
|
||||||
|
|
||||||
public static class ChampionTitleSystem
|
public class ChampionTitleSystem : GenericPersistence
|
||||||
{
|
{
|
||||||
|
private static ChampionTitleSystem _championTitlePersistence;
|
||||||
|
|
||||||
// All of the players with murders
|
// All of the players with murders
|
||||||
private static readonly Dictionary<PlayerMobile, ChampionTitleContext> _championTitleContexts = new();
|
private static readonly Dictionary<PlayerMobile, ChampionTitleContext> _championTitleContexts = new();
|
||||||
|
|
||||||
|
|
@ -15,7 +17,7 @@ public static class ChampionTitleSystem
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
GenericPersistence.Register("ChampionTitles", Serialize, Deserialize);
|
_championTitlePersistence = new ChampionTitleSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
|
|
@ -33,7 +35,11 @@ public static class ChampionTitleSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public ChampionTitleSystem() : base("ChampionTitles", 10)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
var version = reader.ReadEncodedInt();
|
var version = reader.ReadEncodedInt();
|
||||||
|
|
||||||
|
|
@ -47,7 +53,7 @@ public static class ChampionTitleSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(0); // version
|
writer.WriteEncodedInt(0); // version
|
||||||
|
|
||||||
|
|
@ -59,10 +65,10 @@ public static class ChampionTitleSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetChampionTitleContext(this PlayerMobile player, out ChampionTitleContext context) =>
|
public static bool GetChampionTitleContext(PlayerMobile player, out ChampionTitleContext context) =>
|
||||||
_championTitleContexts.TryGetValue(player, out context);
|
_championTitleContexts.TryGetValue(player, out context);
|
||||||
|
|
||||||
public static ChampionTitleContext GetOrCreateChampionTitleContext(this PlayerMobile player)
|
public static ChampionTitleContext GetOrCreateChampionTitleContext(PlayerMobile player)
|
||||||
{
|
{
|
||||||
ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_championTitleContexts, player, out var exists);
|
ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_championTitleContexts, player, out var exists);
|
||||||
if (!exists)
|
if (!exists)
|
||||||
|
|
@ -76,7 +82,7 @@ public static class ChampionTitleSystem
|
||||||
// Called when killing a harrower. Will give a minimum of 1 point.
|
// Called when killing a harrower. Will give a minimum of 1 point.
|
||||||
public static void AwardHarrowerTitle(PlayerMobile pm)
|
public static void AwardHarrowerTitle(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var context = pm.GetOrCreateChampionTitleContext();
|
var context = GetOrCreateChampionTitleContext(pm);
|
||||||
|
|
||||||
var count = 1;
|
var count = 1;
|
||||||
for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++)
|
for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++)
|
||||||
|
|
@ -91,9 +97,9 @@ public static class ChampionTitleSystem
|
||||||
context.Harrower = Math.Max(count, context.Harrower); // Harrower titles never decay.
|
context.Harrower = Math.Max(count, context.Harrower); // Harrower titles never decay.
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetChampionTitleLabel(this PlayerMobile player)
|
public static int GetChampionTitleLabel(PlayerMobile player)
|
||||||
{
|
{
|
||||||
if (!player.GetChampionTitleContext(out var context))
|
if (!GetChampionTitleContext(player, out var context))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ using System;
|
||||||
|
|
||||||
namespace Server.Factions;
|
namespace Server.Factions;
|
||||||
|
|
||||||
public static class FactionSystem
|
public class FactionSystem : GenericPersistence
|
||||||
{
|
{
|
||||||
|
private static FactionSystem _factionSystem;
|
||||||
public static bool Enabled { get; private set; }
|
public static bool Enabled { get; private set; }
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
|
|
@ -12,10 +13,14 @@ public static class FactionSystem
|
||||||
|
|
||||||
if (Enabled)
|
if (Enabled)
|
||||||
{
|
{
|
||||||
GenericPersistence.Register("Factions", Serialize, Deserialize);
|
_factionSystem = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FactionSystem() : base("Factions", 10)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// This does not do the actual work of removing faction stuff, only turns off the persistence.
|
// This does not do the actual work of removing faction stuff, only turns off the persistence.
|
||||||
public static void Disable()
|
public static void Disable()
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +29,7 @@ public static class FactionSystem
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Persistence.Unregister("Factions");
|
_factionSystem.Unregister();
|
||||||
Enabled = false;
|
Enabled = false;
|
||||||
ServerConfiguration.SetSetting("factions.enabled", false);
|
ServerConfiguration.SetSetting("factions.enabled", false);
|
||||||
}
|
}
|
||||||
|
|
@ -37,12 +42,12 @@ public static class FactionSystem
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GenericPersistence.Register("Factions", Serialize, Deserialize);
|
_factionSystem.Register();
|
||||||
Enabled = true;
|
Enabled = true;
|
||||||
ServerConfiguration.SetSetting("factions.enabled", true);
|
ServerConfiguration.SetSetting("factions.enabled", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(0); // version
|
writer.WriteEncodedInt(0); // version
|
||||||
|
|
||||||
|
|
@ -59,7 +64,7 @@ public static class FactionSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
var version = reader.ReadEncodedInt();
|
var version = reader.ReadEncodedInt();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ using Server.Mobiles;
|
||||||
|
|
||||||
namespace Server.Engines.PlayerMurderSystem;
|
namespace Server.Engines.PlayerMurderSystem;
|
||||||
|
|
||||||
public static class PlayerMurderSystem
|
public class PlayerMurderSystem : GenericPersistence
|
||||||
{
|
{
|
||||||
|
private static PlayerMurderSystem _playerMurderPersistence;
|
||||||
|
|
||||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(PlayerMurderSystem));
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(PlayerMurderSystem));
|
||||||
|
|
||||||
// All of the players with murders
|
// All of the players with murders
|
||||||
|
|
@ -28,10 +30,10 @@ public static class PlayerMurderSystem
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
GenericPersistence.Register("PlayerMurders", Serialize, Deserialize);
|
|
||||||
|
|
||||||
_shortTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.shortTermMurderDuration", TimeSpan.FromHours(8));
|
_shortTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.shortTermMurderDuration", TimeSpan.FromHours(8));
|
||||||
_longTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.longTermMurderDuration", TimeSpan.FromHours(40));
|
_longTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.longTermMurderDuration", TimeSpan.FromHours(40));
|
||||||
|
|
||||||
|
_playerMurderPersistence = new PlayerMurderSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
|
|
@ -49,6 +51,10 @@ public static class PlayerMurderSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PlayerMurderSystem() : base("PlayerMurders", 10)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// Only used for migrations!
|
// Only used for migrations!
|
||||||
public static void MigrateContext(PlayerMobile player, TimeSpan shortTerm, TimeSpan longTerm)
|
public static void MigrateContext(PlayerMobile player, TimeSpan shortTerm, TimeSpan longTerm)
|
||||||
{
|
{
|
||||||
|
|
@ -61,7 +67,7 @@ public static class PlayerMurderSystem
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var context = player.GetOrCreateMurderContext();
|
var context = GetOrCreateMurderContext(player);
|
||||||
|
|
||||||
// We make a big assumption that by the time this is called, the Mobile/PlayerMobile info is deserialized
|
// We make a big assumption that by the time this is called, the Mobile/PlayerMobile info is deserialized
|
||||||
if (Mobile.MurderMigrations?.TryGetValue(player, out var shortTermMurders) == true)
|
if (Mobile.MurderMigrations?.TryGetValue(player, out var shortTermMurders) == true)
|
||||||
|
|
@ -100,7 +106,7 @@ public static class PlayerMurderSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
var version = reader.ReadEncodedInt();
|
var version = reader.ReadEncodedInt();
|
||||||
|
|
||||||
|
|
@ -114,7 +120,7 @@ public static class PlayerMurderSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(0); // version
|
writer.WriteEncodedInt(0); // version
|
||||||
|
|
||||||
|
|
@ -126,10 +132,10 @@ public static class PlayerMurderSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetMurderContext(this PlayerMobile player, out MurderContext context) =>
|
public static bool GetMurderContext(PlayerMobile player, out MurderContext context) =>
|
||||||
_murderContexts.TryGetValue(player, out context);
|
_murderContexts.TryGetValue(player, out context);
|
||||||
|
|
||||||
public static MurderContext GetOrCreateMurderContext(this PlayerMobile player)
|
public static MurderContext GetOrCreateMurderContext(PlayerMobile player)
|
||||||
{
|
{
|
||||||
ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_murderContexts, player, out var exists);
|
ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_murderContexts, player, out var exists);
|
||||||
if (!exists)
|
if (!exists)
|
||||||
|
|
@ -142,14 +148,14 @@ public static class PlayerMurderSystem
|
||||||
|
|
||||||
public static void ManuallySetShortTermMurders(PlayerMobile player, int shortTermMurders)
|
public static void ManuallySetShortTermMurders(PlayerMobile player, int shortTermMurders)
|
||||||
{
|
{
|
||||||
var context = player.GetOrCreateMurderContext();
|
var context = GetOrCreateMurderContext(player);
|
||||||
context.ShortTermMurders = shortTermMurders;
|
context.ShortTermMurders = shortTermMurders;
|
||||||
UpdateMurderContext(context);
|
UpdateMurderContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void OnPlayerMurder(PlayerMobile player)
|
public static void OnPlayerMurder(PlayerMobile player)
|
||||||
{
|
{
|
||||||
var context = player.GetOrCreateMurderContext();
|
var context = GetOrCreateMurderContext(player);
|
||||||
context.ShortTermMurders++;
|
context.ShortTermMurders++;
|
||||||
player.Kills++;
|
player.Kills++;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ using Server.Utilities;
|
||||||
|
|
||||||
namespace Server.Engines.Stealables;
|
namespace Server.Engines.Stealables;
|
||||||
|
|
||||||
public static class StealableArtifacts
|
public class StealableArtifacts : GenericPersistence
|
||||||
{
|
{
|
||||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(StealableArtifacts));
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(StealableArtifacts));
|
||||||
|
|
||||||
|
private static StealableArtifacts _stealableArtifactsPersistence;
|
||||||
private static bool _enabled;
|
private static bool _enabled;
|
||||||
private static Type[] _typesOfEntries;
|
private static Type[] _typesOfEntries;
|
||||||
private static StealableInstance[] _artifacts;
|
private static StealableInstance[] _artifacts;
|
||||||
|
|
@ -19,7 +20,11 @@ public static class StealableArtifacts
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
GenericPersistence.Register("StealableArtifacts", Serialize, Deserialize);
|
_stealableArtifactsPersistence = new StealableArtifacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StealableArtifacts() : base("StealableArtifacts", 10)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveStealableArtifacts()
|
private static void RemoveStealableArtifacts()
|
||||||
|
|
@ -242,7 +247,7 @@ public static class StealableArtifacts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(1); // version
|
writer.WriteEncodedInt(1); // version
|
||||||
|
|
||||||
|
|
@ -262,7 +267,7 @@ public static class StealableArtifacts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
var version = reader.ReadEncodedInt();
|
var version = reader.ReadEncodedInt();
|
||||||
|
|
||||||
|
|
@ -441,7 +446,7 @@ public static class StealableArtifacts
|
||||||
{
|
{
|
||||||
base.Deserialize(reader);
|
base.Deserialize(reader);
|
||||||
|
|
||||||
StealableArtifacts.Deserialize(reader);
|
_stealableArtifactsPersistence.Deserialize(reader);
|
||||||
|
|
||||||
Timer.DelayCall(Delete);
|
Timer.DelayCall(Delete);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public static class CompassionVirtue
|
||||||
|
|
||||||
public static void CheckAtrophy(PlayerMobile pm)
|
public static void CheckAtrophy(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetVirtues();
|
var virtues = VirtueSystem.GetVirtues(pm);
|
||||||
if (virtues?.Compassion > 0 && CanAtrophy(virtues))
|
if (virtues?.Compassion > 0 && CanAtrophy(virtues))
|
||||||
{
|
{
|
||||||
if (VirtueSystem.Atrophy(pm, VirtueName.Compassion, LossAmount))
|
if (VirtueSystem.Atrophy(pm, VirtueName.Compassion, LossAmount))
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public static class HonorVirtue
|
||||||
|
|
||||||
private static void EmbraceHonor(PlayerMobile pm)
|
private static void EmbraceHonor(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetVirtues();
|
var virtues = VirtueSystem.GetVirtues(pm);
|
||||||
|
|
||||||
if (virtues?.HonorActive == true)
|
if (virtues?.HonorActive == true)
|
||||||
{
|
{
|
||||||
|
|
@ -73,7 +73,7 @@ public static class HonorVirtue
|
||||||
public static void ActivateEmbrace(PlayerMobile pm)
|
public static void ActivateEmbrace(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var duration = GetHonorDuration(pm);
|
var duration = GetHonorDuration(pm);
|
||||||
var virtues = pm.GetOrCreateVirtues();
|
var virtues = VirtueSystem.GetOrCreateVirtues(pm);
|
||||||
|
|
||||||
int usedPoints = virtues.Honor switch
|
int usedPoints = virtues.Honor switch
|
||||||
{
|
{
|
||||||
|
|
@ -92,7 +92,7 @@ public static class HonorVirtue
|
||||||
(m) =>
|
(m) =>
|
||||||
{
|
{
|
||||||
// We get the virtues again, in case it was deleted/dereferenced
|
// We get the virtues again, in case it was deleted/dereferenced
|
||||||
var v = m.GetOrCreateVirtues();
|
var v = VirtueSystem.GetOrCreateVirtues(m);
|
||||||
v.HonorActive = false;
|
v.HonorActive = false;
|
||||||
v.LastHonorUse = Core.Now;
|
v.LastHonorUse = Core.Now;
|
||||||
m.SendLocalizedMessage(1063236); // You no longer embrace your honor
|
m.SendLocalizedMessage(1063236); // You no longer embrace your honor
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ public class HonorContext
|
||||||
Source.Mana += restore;
|
Source.Mana += restore;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Source.GetVirtues().Honor > targetFame)
|
if (VirtueSystem.GetVirtues(Source).Honor > targetFame)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,23 +32,23 @@ public class JusticeVirtue
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsProtected(PlayerMobile pm) =>
|
public static bool IsProtected(PlayerMobile pm) =>
|
||||||
pm.GetVirtues() is { JusticeStatus: JusticeProtectorStatus.Protected, JusticeProtection: not null };
|
VirtueSystem.GetVirtues(pm) is { JusticeStatus: JusticeProtectorStatus.Protected, JusticeProtection: not null };
|
||||||
|
|
||||||
public static PlayerMobile GetProtector(PlayerMobile pm) =>
|
public static PlayerMobile GetProtector(PlayerMobile pm) =>
|
||||||
pm.GetVirtues() is { JusticeStatus: JusticeProtectorStatus.Protected } virtues ? virtues.JusticeProtection : null;
|
VirtueSystem.GetVirtues(pm) is { JusticeStatus: JusticeProtectorStatus.Protected } virtues ? virtues.JusticeProtection : null;
|
||||||
|
|
||||||
public static PlayerMobile GetProtected(PlayerMobile pm) =>
|
public static PlayerMobile GetProtected(PlayerMobile pm) =>
|
||||||
pm.GetVirtues() is { JusticeStatus: JusticeProtectorStatus.Protector } virtues ? virtues.JusticeProtection : null;
|
VirtueSystem.GetVirtues(pm) is { JusticeStatus: JusticeProtectorStatus.Protector } virtues ? virtues.JusticeProtection : null;
|
||||||
|
|
||||||
public static void CancelProtection(PlayerMobile pm)
|
public static void CancelProtection(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
if (pm.GetVirtues() is { JusticeStatus: not JusticeProtectorStatus.None } virtues)
|
if (VirtueSystem.GetVirtues(pm) is { JusticeStatus: not JusticeProtectorStatus.None } virtues)
|
||||||
{
|
{
|
||||||
var protector = virtues.JusticeProtection;
|
var protector = virtues.JusticeProtection;
|
||||||
virtues.JusticeProtection = null;
|
virtues.JusticeProtection = null;
|
||||||
virtues.JusticeStatus = JusticeProtectorStatus.None;
|
virtues.JusticeStatus = JusticeProtectorStatus.None;
|
||||||
|
|
||||||
virtues = protector?.GetVirtues();
|
virtues = VirtueSystem.GetVirtues(protector);
|
||||||
if (virtues != null)
|
if (virtues != null)
|
||||||
{
|
{
|
||||||
virtues.JusticeProtection = null;
|
virtues.JusticeProtection = null;
|
||||||
|
|
@ -59,13 +59,13 @@ public class JusticeVirtue
|
||||||
|
|
||||||
public static bool CancelProtection(PlayerMobile pm, out PlayerMobile protector)
|
public static bool CancelProtection(PlayerMobile pm, out PlayerMobile protector)
|
||||||
{
|
{
|
||||||
if (pm.GetVirtues() is { JusticeStatus: not JusticeProtectorStatus.None } virtues)
|
if (VirtueSystem.GetVirtues(pm) is { JusticeStatus: not JusticeProtectorStatus.None } virtues)
|
||||||
{
|
{
|
||||||
protector = virtues.JusticeProtection;
|
protector = virtues.JusticeProtection;
|
||||||
virtues.JusticeProtection = null;
|
virtues.JusticeProtection = null;
|
||||||
virtues.JusticeStatus = JusticeProtectorStatus.None;
|
virtues.JusticeStatus = JusticeProtectorStatus.None;
|
||||||
|
|
||||||
virtues = protector?.GetVirtues();
|
virtues = VirtueSystem.GetVirtues(protector);
|
||||||
if (virtues != null)
|
if (virtues != null)
|
||||||
{
|
{
|
||||||
virtues.JusticeProtection = null;
|
virtues.JusticeProtection = null;
|
||||||
|
|
@ -255,7 +255,7 @@ public class JusticeVirtue
|
||||||
|
|
||||||
public static void CheckAtrophy(PlayerMobile pm)
|
public static void CheckAtrophy(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetVirtues();
|
var virtues = VirtueSystem.GetVirtues(pm);
|
||||||
if (virtues?.Justice > 0 && CanAtrophy(virtues))
|
if (virtues?.Justice > 0 && CanAtrophy(virtues))
|
||||||
{
|
{
|
||||||
if (VirtueSystem.Atrophy(pm, VirtueName.Justice, LossAmount))
|
if (VirtueSystem.Atrophy(pm, VirtueName.Justice, LossAmount))
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public static class SacrificeVirtue
|
||||||
|
|
||||||
public static void CheckAtrophy(PlayerMobile pm)
|
public static void CheckAtrophy(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetVirtues();
|
var virtues = VirtueSystem.GetVirtues(pm);
|
||||||
if (virtues?.Sacrifice > 0 && CanAtrophy(virtues))
|
if (virtues?.Sacrifice > 0 && CanAtrophy(virtues))
|
||||||
{
|
{
|
||||||
if (VirtueSystem.Atrophy(pm, VirtueName.Sacrifice, LossAmount))
|
if (VirtueSystem.Atrophy(pm, VirtueName.Sacrifice, LossAmount))
|
||||||
|
|
@ -67,7 +67,7 @@ public static class SacrificeVirtue
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var virtues = from.GetVirtues();
|
var virtues = VirtueSystem.GetVirtues(from);
|
||||||
if (virtues?.AvailableResurrects > 0)
|
if (virtues?.AvailableResurrects > 0)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|
|
@ -124,7 +124,7 @@ public static class SacrificeVirtue
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var virtues = from.GetOrCreateVirtues();
|
var virtues = VirtueSystem.GetOrCreateVirtues(from);
|
||||||
if (!CanGain(virtues))
|
if (!CanGain(virtues))
|
||||||
{
|
{
|
||||||
from.SendLocalizedMessage(1052016); // You must wait approximately one day before sacrificing again.
|
from.SendLocalizedMessage(1052016); // You must wait approximately one day before sacrificing again.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public static class ValorVirtue
|
||||||
|
|
||||||
public static void CheckAtrophy(PlayerMobile pm)
|
public static void CheckAtrophy(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetVirtues();
|
var virtues = VirtueSystem.GetVirtues(pm);
|
||||||
if (virtues?.Valor > 0 && CanAtrophy(virtues))
|
if (virtues?.Valor > 0 && CanAtrophy(virtues))
|
||||||
{
|
{
|
||||||
if (VirtueSystem.Atrophy(pm, VirtueName.Valor, LossAmount))
|
if (VirtueSystem.Atrophy(pm, VirtueName.Valor, LossAmount))
|
||||||
|
|
@ -90,7 +90,7 @@ public static class ValorVirtue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (from.GetVirtues()?.GetValue((int)VirtueName.Valor) >= needed)
|
if (VirtueSystem.GetVirtues(from)?.GetValue((int)VirtueName.Valor) >= needed)
|
||||||
{
|
{
|
||||||
VirtueSystem.Atrophy(from, VirtueName.Valor, consumed);
|
VirtueSystem.Atrophy(from, VirtueName.Valor, consumed);
|
||||||
// Your challenge is heard by the Champion of this region! Beware its wrath!
|
// Your challenge is heard by the Champion of this region! Beware its wrath!
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ public class VirtueGump : Gump
|
||||||
|
|
||||||
private int GetHueFor(int index)
|
private int GetHueFor(int index)
|
||||||
{
|
{
|
||||||
var value = _beheld.GetVirtues()?.GetValue(index) ?? 0;
|
var value = VirtueSystem.GetVirtues((_beheld))?.GetValue(index) ?? 0;
|
||||||
|
|
||||||
if (value < 4000)
|
if (value < 4000)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public class VirtueInfoGump : Gump
|
||||||
_desc = description;
|
_desc = description;
|
||||||
_site = webPage;
|
_site = webPage;
|
||||||
|
|
||||||
var value = beholder.GetVirtues()?.GetValue((int)virtue) ?? 0;
|
var value = VirtueSystem.GetVirtues(beholder)?.GetValue((int)virtue) ?? 0;
|
||||||
|
|
||||||
AddPage(0);
|
AddPage(0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ public class VirtueStatusGump : Gump
|
||||||
_beholder,
|
_beholder,
|
||||||
virtue,
|
virtue,
|
||||||
GetVirtueDescription(virtue),
|
GetVirtueDescription(virtue),
|
||||||
@$"https://uo.com/wiki/ultima-online-wiki/gameplay/the-virtues/#{virtue.GetLowerCaseName()}"
|
@$"https://uo.com/wiki/ultima-online-wiki/gameplay/the-virtues/#{VirtueSystem.GetLowerCaseName(virtue)}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,23 @@ public enum VirtueName
|
||||||
Honesty
|
Honesty
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class VirtueSystem
|
public class VirtueSystem : GenericPersistence
|
||||||
{
|
{
|
||||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(VirtueSystem));
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(VirtueSystem));
|
||||||
|
|
||||||
private static readonly Dictionary<PlayerMobile, VirtueContext> _playerVirtues = new();
|
private static readonly Dictionary<PlayerMobile, VirtueContext> _playerVirtues = new();
|
||||||
|
|
||||||
|
private static VirtueSystem _virtueSystemPersistence;
|
||||||
|
|
||||||
|
public static void Configure()
|
||||||
|
{
|
||||||
|
_virtueSystemPersistence = new VirtueSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtueSystem() : base("Virtues", 10)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private static void FixVirtue(Mobile m, int[] virtueValues)
|
private static void FixVirtue(Mobile m, int[] virtueValues)
|
||||||
{
|
{
|
||||||
if (m is not PlayerMobile pm)
|
if (m is not PlayerMobile pm)
|
||||||
|
|
@ -52,11 +63,6 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Configure()
|
|
||||||
{
|
|
||||||
GenericPersistence.Register("Virtues", Serialize, Deserialize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
var migrations = Mobile.VirtueMigrations;
|
var migrations = Mobile.VirtueMigrations;
|
||||||
|
|
@ -69,7 +75,7 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(0); // version
|
writer.WriteEncodedInt(0); // version
|
||||||
|
|
||||||
|
|
@ -81,7 +87,7 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
reader.ReadEncodedInt(); // version
|
reader.ReadEncodedInt(); // version
|
||||||
|
|
||||||
|
|
@ -99,10 +105,10 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VirtueContext GetVirtues(this PlayerMobile from) =>
|
public static VirtueContext GetVirtues(PlayerMobile from) =>
|
||||||
_playerVirtues.TryGetValue(from, out var context) ? context : null;
|
_playerVirtues.TryGetValue(from, out var context) ? context : null;
|
||||||
|
|
||||||
public static VirtueContext GetOrCreateVirtues(this PlayerMobile from)
|
public static VirtueContext GetOrCreateVirtues(PlayerMobile from)
|
||||||
{
|
{
|
||||||
ref VirtueContext context = ref CollectionsMarshal.GetValueRefOrAddDefault(_playerVirtues, from, out bool exists);
|
ref VirtueContext context = ref CollectionsMarshal.GetValueRefOrAddDefault(_playerVirtues, from, out bool exists);
|
||||||
if (!exists)
|
if (!exists)
|
||||||
|
|
@ -114,11 +120,11 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsHighestPath(PlayerMobile from, VirtueName virtue) =>
|
public static bool IsHighestPath(PlayerMobile from, VirtueName virtue) =>
|
||||||
from.GetVirtues()?.GetValue((int)virtue) >= GetMaxAmount(virtue);
|
GetVirtues(from)?.GetValue((int)virtue) >= GetMaxAmount(virtue);
|
||||||
|
|
||||||
public static VirtueLevel GetLevel(Mobile from, VirtueName virtue)
|
public static VirtueLevel GetLevel(Mobile from, VirtueName virtue)
|
||||||
{
|
{
|
||||||
var v = (from as PlayerMobile)?.GetVirtues()?.GetValue((int)virtue) ?? 0;
|
var v = GetVirtues(from as PlayerMobile)?.GetValue((int)virtue) ?? 0;
|
||||||
int vl;
|
int vl;
|
||||||
|
|
||||||
if (v < 4000)
|
if (v < 4000)
|
||||||
|
|
@ -137,7 +143,7 @@ public static class VirtueSystem
|
||||||
return (VirtueLevel)vl;
|
return (VirtueLevel)vl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetName(this VirtueName virtue) =>
|
public static string GetName(VirtueName virtue) =>
|
||||||
virtue switch
|
virtue switch
|
||||||
{
|
{
|
||||||
VirtueName.Humility => "Humility",
|
VirtueName.Humility => "Humility",
|
||||||
|
|
@ -151,7 +157,7 @@ public static class VirtueSystem
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string GetLowerCaseName(this VirtueName virtue) =>
|
public static string GetLowerCaseName(VirtueName virtue) =>
|
||||||
virtue switch
|
virtue switch
|
||||||
{
|
{
|
||||||
VirtueName.Humility => "humility",
|
VirtueName.Humility => "humility",
|
||||||
|
|
@ -240,7 +246,7 @@ public static class VirtueSystem
|
||||||
|
|
||||||
public static bool Atrophy(PlayerMobile from, VirtueName virtue, int amount = 1)
|
public static bool Atrophy(PlayerMobile from, VirtueName virtue, int amount = 1)
|
||||||
{
|
{
|
||||||
var virtues = from.GetVirtues();
|
var virtues = GetVirtues(from);
|
||||||
if (virtues == null)
|
if (virtues == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -268,7 +274,7 @@ public static class VirtueSystem
|
||||||
|
|
||||||
public static void AwardVirtue(PlayerMobile pm, VirtueName virtue, int amount)
|
public static void AwardVirtue(PlayerMobile pm, VirtueName virtue, int amount)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetOrCreateVirtues();
|
var virtues = GetOrCreateVirtues(pm);
|
||||||
if (virtue == VirtueName.Compassion)
|
if (virtue == VirtueName.Compassion)
|
||||||
{
|
{
|
||||||
if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay)
|
if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay)
|
||||||
|
|
@ -285,7 +291,7 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
var gainedPath = false;
|
var gainedPath = false;
|
||||||
var virtueName = virtue.GetName();
|
var virtueName = GetName(virtue);
|
||||||
|
|
||||||
if (Award(pm, virtue, amount, ref gainedPath))
|
if (Award(pm, virtue, amount, ref gainedPath))
|
||||||
{
|
{
|
||||||
|
|
@ -331,7 +337,7 @@ public static class VirtueSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CheckAtrophies(this PlayerMobile pm)
|
public static void CheckAtrophies(PlayerMobile pm)
|
||||||
{
|
{
|
||||||
SacrificeVirtue.CheckAtrophy(pm);
|
SacrificeVirtue.CheckAtrophy(pm);
|
||||||
JusticeVirtue.CheckAtrophy(pm);
|
JusticeVirtue.CheckAtrophy(pm);
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,21 @@ using Server.Mobiles;
|
||||||
|
|
||||||
namespace Server.Items;
|
namespace Server.Items;
|
||||||
|
|
||||||
public static class DisguisePersistence
|
public class DisguisePersistence : GenericPersistence
|
||||||
{
|
{
|
||||||
|
private static DisguisePersistence _disguisePersistence;
|
||||||
public static Dictionary<Mobile, Timer> Timers { get; } = new();
|
public static Dictionary<Mobile, Timer> Timers { get; } = new();
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
GenericPersistence.Register("Disguises", Serialize, Deserialize);
|
_disguisePersistence = new DisguisePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public DisguisePersistence() : base("Disguises", 10)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
var count = reader.ReadEncodedInt();
|
var count = reader.ReadEncodedInt();
|
||||||
for (var i = 0; i < count; ++i)
|
for (var i = 0; i < count; ++i)
|
||||||
|
|
@ -24,7 +29,7 @@ public static class DisguisePersistence
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(Timers.Count);
|
writer.WriteEncodedInt(Timers.Count);
|
||||||
foreach (var (m, timer) in Timers)
|
foreach (var (m, timer) in Timers)
|
||||||
|
|
|
||||||
|
|
@ -1857,7 +1857,7 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab
|
||||||
|
|
||||||
if (attacker is PlayerMobile pmAttacker && !(Core.ML && defender is PlayerMobile))
|
if (attacker is PlayerMobile pmAttacker && !(Core.ML && defender is PlayerMobile))
|
||||||
{
|
{
|
||||||
if (pmAttacker.GetVirtues()?.HonorActive == true && pmAttacker.InRange(defender, 1))
|
if (VirtueSystem.GetVirtues(pmAttacker)?.HonorActive == true && pmAttacker.InRange(defender, 1))
|
||||||
{
|
{
|
||||||
percentageBonus += 25;
|
percentageBonus += 25;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public enum DFAlgorithm
|
||||||
PainSpike
|
PainSpike
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class StaminaSystem
|
public class StaminaSystem : GenericPersistence
|
||||||
{
|
{
|
||||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(StaminaSystem));
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(StaminaSystem));
|
||||||
|
|
||||||
|
|
@ -44,11 +44,13 @@ public static class StaminaSystem
|
||||||
AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10);
|
AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10);
|
||||||
EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true);
|
EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true);
|
||||||
UseMountStaminaOnlyWhenOverloaded = ServerConfiguration.GetSetting("stamina.useMountStaminaOnlyWhenOverloaded", Core.SA);
|
UseMountStaminaOnlyWhenOverloaded = ServerConfiguration.GetSetting("stamina.useMountStaminaOnlyWhenOverloaded", Core.SA);
|
||||||
|
|
||||||
GenericPersistence.Register("StaminaSystem", Serialize, Deserialize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Serialize(IGenericWriter writer)
|
public StaminaSystem() : base("StaminaSystem", 10)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteEncodedInt(0); // version
|
writer.WriteEncodedInt(0); // version
|
||||||
|
|
||||||
|
|
@ -60,7 +62,7 @@ public static class StaminaSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Deserialize(IGenericReader reader)
|
public override void Deserialize(IGenericReader reader)
|
||||||
{
|
{
|
||||||
var version = reader.ReadEncodedInt();
|
var version = reader.ReadEncodedInt();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,7 @@ namespace Server.Misc
|
||||||
|
|
||||||
if (beheld is PlayerMobile mobile && mobile.DisplayChampionTitle)
|
if (beheld is PlayerMobile mobile && mobile.DisplayChampionTitle)
|
||||||
{
|
{
|
||||||
var titleLabel = mobile.GetChampionTitleLabel();
|
var titleLabel = ChampionTitleSystem.GetChampionTitleLabel(mobile);
|
||||||
if (titleLabel > 0)
|
if (titleLabel > 0)
|
||||||
{
|
{
|
||||||
// Should this be translated to the receivers language? Prefix titles aren't?
|
// Should this be translated to the receivers language? Prefix titles aren't?
|
||||||
|
|
|
||||||
|
|
@ -2702,7 +2702,7 @@ public abstract class BaseAI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore players with activated honor
|
// Ignore players with activated honor
|
||||||
if (m_Mobile.Combatant != m && pm?.GetVirtues()?.HonorActive == true)
|
if (m_Mobile.Combatant != m && VirtueSystem.GetVirtues(pm)?.HonorActive == true)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1288,7 +1288,7 @@ namespace Server.Mobiles
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((m as PlayerMobile)?.GetVirtues()?.HonorActive == true)
|
if (VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -697,30 +697,32 @@ namespace Server.Mobiles
|
||||||
}
|
}
|
||||||
|
|
||||||
[CommandProperty(AccessLevel.GameMaster, canModify: true)]
|
[CommandProperty(AccessLevel.GameMaster, canModify: true)]
|
||||||
public ChampionTitleContext ChampionTitles => this.GetOrCreateChampionTitleContext();
|
public ChampionTitleContext ChampionTitles => ChampionTitleSystem.GetOrCreateChampionTitleContext(this);
|
||||||
|
|
||||||
[CommandProperty(AccessLevel.GameMaster)]
|
[CommandProperty(AccessLevel.GameMaster)]
|
||||||
public int ShortTermMurders
|
public int ShortTermMurders
|
||||||
{
|
{
|
||||||
get => this.GetMurderContext(out var context) ? context.ShortTermMurders : 0;
|
get => PlayerMurderSystem.GetMurderContext(this, out var context) ? context.ShortTermMurders : 0;
|
||||||
set => PlayerMurderSystem.ManuallySetShortTermMurders(this, value);
|
set => PlayerMurderSystem.ManuallySetShortTermMurders(this, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[CommandProperty(AccessLevel.GameMaster)]
|
[CommandProperty(AccessLevel.GameMaster)]
|
||||||
public DateTime ShortTermMurderExpiration => this.GetMurderContext(out var context) && context.ShortTermMurders > 0
|
public DateTime ShortTermMurderExpiration
|
||||||
? Core.Now + (context.ShortTermElapse - GameTime)
|
=> PlayerMurderSystem.GetMurderContext(this, out var context) && context.ShortTermMurders > 0
|
||||||
: DateTime.MinValue;
|
? Core.Now + (context.ShortTermElapse - GameTime)
|
||||||
|
: DateTime.MinValue;
|
||||||
|
|
||||||
[CommandProperty(AccessLevel.GameMaster)]
|
[CommandProperty(AccessLevel.GameMaster)]
|
||||||
public DateTime LongTermMurderExpiration => Kills > 0 && this.GetMurderContext(out var context)
|
public DateTime LongTermMurderExpiration
|
||||||
? Core.Now + (context.LongTermElapse - GameTime)
|
=> Kills > 0 && PlayerMurderSystem.GetMurderContext(this, out var context)
|
||||||
: DateTime.MinValue;
|
? Core.Now + (context.LongTermElapse - GameTime)
|
||||||
|
: DateTime.MinValue;
|
||||||
|
|
||||||
[CommandProperty(AccessLevel.GameMaster)]
|
[CommandProperty(AccessLevel.GameMaster)]
|
||||||
public int KnownRecipes => m_AcquiredRecipes?.Count ?? 0;
|
public int KnownRecipes => m_AcquiredRecipes?.Count ?? 0;
|
||||||
|
|
||||||
[CommandProperty(AccessLevel.Counselor, canModify: true)]
|
[CommandProperty(AccessLevel.Counselor, canModify: true)]
|
||||||
public VirtueContext Virtues => this.GetOrCreateVirtues();
|
public VirtueContext Virtues => VirtueSystem.GetOrCreateVirtues(this);
|
||||||
|
|
||||||
public HonorContext ReceivedHonorContext { get; set; }
|
public HonorContext ReceivedHonorContext { get; set; }
|
||||||
|
|
||||||
|
|
@ -1255,7 +1257,7 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
if (from is PlayerMobile mobile)
|
if (from is PlayerMobile mobile)
|
||||||
{
|
{
|
||||||
mobile.CheckAtrophies();
|
VirtueSystem.CheckAtrophies(mobile);
|
||||||
mobile.ClaimAutoStabledPets();
|
mobile.ClaimAutoStabledPets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3426,7 +3428,7 @@ namespace Server.Mobiles
|
||||||
// https://uo.com/wiki/ultima-online-wiki/player/skill-titles-order/
|
// https://uo.com/wiki/ultima-online-wiki/player/skill-titles-order/
|
||||||
if (DisplayChampionTitle)
|
if (DisplayChampionTitle)
|
||||||
{
|
{
|
||||||
var titleLabel = this.GetChampionTitleLabel();
|
var titleLabel = ChampionTitleSystem.GetChampionTitleLabel(this);
|
||||||
if (titleLabel > 0)
|
if (titleLabel > 0)
|
||||||
{
|
{
|
||||||
list.Add(titleLabel);
|
list.Add(titleLabel);
|
||||||
|
|
|
||||||
|
|
@ -643,7 +643,7 @@ public partial class BaseEscortable : BaseCreature
|
||||||
|
|
||||||
if (escorter is PlayerMobile pm)
|
if (escorter is PlayerMobile pm)
|
||||||
{
|
{
|
||||||
var virtues = pm.GetOrCreateVirtues();
|
var virtues = VirtueSystem.GetOrCreateVirtues(pm);
|
||||||
if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay)
|
if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay)
|
||||||
{
|
{
|
||||||
virtues.NextCompassionDay = DateTime.MinValue;
|
virtues.NextCompassionDay = DateTime.MinValue;
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ namespace Server.SkillHandlers
|
||||||
creature.AIObject?.DoMove(creature.Direction);
|
creature.AIObject?.DoMove(creature.Direction);
|
||||||
|
|
||||||
if (from is PlayerMobile pm &&
|
if (from is PlayerMobile pm &&
|
||||||
!(pm.GetVirtues()?.HonorActive == true ||
|
!(VirtueSystem.GetVirtues(pm)?.HonorActive == true ||
|
||||||
TransformationSpellHelper.UnderTransformation(pm, typeof(EtherealVoyageSpell))))
|
TransformationSpellHelper.UnderTransformation(pm, typeof(EtherealVoyageSpell))))
|
||||||
{
|
{
|
||||||
creature.Combatant = pm;
|
creature.Combatant = pm;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue