fix(core): Fixes flow for saves, adds logging and configurations (#513)
- [X] Adds better diagnostics for world saves. - [X] Adds world save stats. - [X] World now saves to a temporary folder. - [X] Backups are now stored by timestamp. - [X] Backup is now made after the world save. - [X] "Saves" folder is now configurable. - [X] Temporary folder is now configurable. - [X] Backups folder is now configurable.
This commit is contained in:
parent
6f5312cb89
commit
da01f5b8a5
19 changed files with 325 additions and 210 deletions
|
|
@ -15,6 +15,9 @@ namespace Server.Tests
|
|||
// Configure / Initialize
|
||||
TestMapDefinitions.ConfigureTestMapDefinitions();
|
||||
|
||||
// Configure the world
|
||||
World.Configure();
|
||||
|
||||
// Load the world
|
||||
World.Load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ namespace Server
|
|||
throw new FileNotFoundException($"Failed to deserialize {m_FilePath}.");
|
||||
}
|
||||
|
||||
Utility.PushColor(ConsoleColor.Green);
|
||||
Console.WriteLine("done");
|
||||
Utility.PopColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server
|
||||
|
|
@ -41,6 +42,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<AccountLoginEventArgs> AccountLogin;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeAccountLogin(AccountLoginEventArgs e) => AccountLogin?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
|
|
@ -64,6 +65,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<AggressiveActionEventArgs> AggressiveAction;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeAggressiveAction(AggressiveActionEventArgs e) => AggressiveAction?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Accounting;
|
||||
using Server.Network;
|
||||
|
||||
|
|
@ -102,6 +103,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<CharacterCreatedEventArgs> CharacterCreated;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeCharacterCreated(CharacterCreatedEventArgs e) => CharacterCreated?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,12 +106,6 @@ namespace Server
|
|||
public static event Action<NetState, int> DeleteRequest;
|
||||
public static void InvokeDeleteRequest(NetState state, int index) => DeleteRequest?.Invoke(state, index);
|
||||
|
||||
public static event Action WorldLoad;
|
||||
public static void InvokeWorldLoad() => WorldLoad?.Invoke();
|
||||
|
||||
public static event Action WorldSave;
|
||||
public static void InvokeWorldSave() => WorldSave?.Invoke();
|
||||
|
||||
public static event Action<Mobile, int> SetAbility;
|
||||
public static void InvokeSetAbility(Mobile mobile, int index) => SetAbility?.Invoke(mobile, index);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server
|
||||
|
|
@ -31,6 +32,7 @@ namespace Server
|
|||
{
|
||||
public static event Action<FastWalkEventArgs> FastWalk;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeFastWalk(FastWalkEventArgs e) => FastWalk?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server
|
||||
|
|
@ -41,6 +42,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<GameLoginEventArgs> GameLogin;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeGameLogin(GameLoginEventArgs e) => GameLogin?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
|
|
@ -63,6 +64,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<MovementEventArgs> Movement;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeMovement(MovementEventArgs e) => Movement?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
|
|
@ -29,6 +30,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<ServerCrashedEventArgs> ServerCrashed;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeServerCrashed(ServerCrashedEventArgs e) => ServerCrashed?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Accounting;
|
||||
using Server.Network;
|
||||
|
||||
|
|
@ -52,6 +53,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<ServerListEventArgs> ServerList;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeServerList(ServerListEventArgs e) => ServerList?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
|
|
@ -34,6 +35,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<SocketConnectEventArgs> SocketConnect;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeSocketConnect(SocketConnectEventArgs e) => SocketConnect?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server
|
||||
|
|
@ -60,6 +61,8 @@ namespace Server
|
|||
public static partial class EventSink
|
||||
{
|
||||
public static event Action<SpeechEventArgs> Speech;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeSpeech(SpeechEventArgs e) => Speech?.Invoke(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1432,5 +1432,13 @@ namespace Server
|
|||
result = a - div * b;
|
||||
return div;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string GetTimeStamp()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return $"{now.Year}-{now.Month}-{now.Day} {now.Hour}-{now.Minute:D2}-{now.Second:D2}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using System.Collections.Concurrent;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
|
@ -43,6 +44,9 @@ namespace Server
|
|||
private static readonly Dictionary<Serial, IEntity> _pendingDelete = new();
|
||||
private static readonly ConcurrentQueue<Item> _decayQueue = new();
|
||||
|
||||
private static string _tempSavePath; // Path to the temporary folder for the save
|
||||
private static string _savePath; // Path to "Saves" folder
|
||||
|
||||
public const uint ItemOffset = 0x40000000;
|
||||
public const uint MaxItemSerial = 0x7FFFFFFF;
|
||||
public const uint MaxMobileSerial = ItemOffset - 1;
|
||||
|
|
@ -118,7 +122,6 @@ namespace Server
|
|||
|
||||
private static void OutOfMemory(string message) => throw new OutOfMemoryException(message);
|
||||
|
||||
internal static int _Saves;
|
||||
internal static List<Type> ItemTypes { get; } = new();
|
||||
internal static List<Type> MobileTypes { get; } = new();
|
||||
internal static List<Type> GuildTypes { get; } = new();
|
||||
|
|
@ -132,6 +135,15 @@ namespace Server
|
|||
public static Dictionary<Serial, Item> Items { get; private set; }
|
||||
public static Dictionary<Serial, BaseGuild> Guilds { get; private set; }
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
var tempSavePath = ServerConfiguration.GetOrUpdateSetting("world.tempSavePath", "temp");
|
||||
_tempSavePath = Path.Combine(Core.BaseDirectory, tempSavePath);
|
||||
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
|
||||
_savePath = Path.Combine(Core.BaseDirectory, savePath);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WaitForWriteCompletion()
|
||||
{
|
||||
m_DiskWriteHandle.WaitOne();
|
||||
|
|
@ -176,11 +188,38 @@ namespace Server
|
|||
NetState.FlushAll();
|
||||
}
|
||||
|
||||
public static void Broadcast(int hue, bool ascii, string format, params object[] args)
|
||||
public static void BroadcastStaff(int hue, bool ascii, string text)
|
||||
{
|
||||
Broadcast(hue, ascii, string.Format(format, args));
|
||||
var length = OutgoingMessagePackets.GetMaxMessageLength(text);
|
||||
|
||||
Span<byte> buffer = stackalloc byte[length].InitializePacket();
|
||||
|
||||
foreach (var ns in TcpServer.Instances)
|
||||
{
|
||||
if (ns.Mobile == null || ns.Mobile.AccessLevel < AccessLevel.GameMaster)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
length = OutgoingMessagePackets.CreateMessage(
|
||||
buffer, Serial.MinusOne, -1, MessageType.Regular, hue, 3, ascii, "ENU", "System", text
|
||||
);
|
||||
|
||||
if (length != buffer.Length)
|
||||
{
|
||||
buffer = buffer.SliceToLength(length); // Adjust to the actual size
|
||||
}
|
||||
|
||||
ns.Send(buffer);
|
||||
}
|
||||
|
||||
NetState.FlushAll();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Broadcast(int hue, bool ascii, string format, params object[] args) =>
|
||||
Broadcast(hue, ascii, string.Format(format, args));
|
||||
|
||||
private static List<Tuple<ConstructorInfo, string>> ReadTypes<I>(BinaryReader tdbReader)
|
||||
{
|
||||
var constructorTypes = new[] { typeof(I) };
|
||||
|
|
@ -237,8 +276,8 @@ namespace Server
|
|||
|
||||
var indexType = indexInfo.TypeName;
|
||||
|
||||
string indexPath = Path.Combine("Saves", indexType, $"{indexType}.idx");
|
||||
string typesPath = Path.Combine("Saves", indexType, $"{indexType}.tdb");
|
||||
string indexPath = Path.Combine(_savePath, indexType, $"{indexType}.idx");
|
||||
string typesPath = Path.Combine(_savePath, indexType, $"{indexType}.tdb");
|
||||
|
||||
entities = new List<EntityIndex<T>>();
|
||||
|
||||
|
|
@ -250,45 +289,42 @@ namespace Server
|
|||
using FileStream idx = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
BinaryReader idxReader = new BinaryReader(idx);
|
||||
|
||||
using (FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
BinaryReader tdbReader = new BinaryReader(tdb);
|
||||
|
||||
List<Tuple<ConstructorInfo, string>> types = ReadTypes<I>(tdbReader);
|
||||
|
||||
var count = idxReader.ReadInt32();
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
BinaryReader tdbReader = new BinaryReader(tdb);
|
||||
var typeID = idxReader.ReadInt32();
|
||||
var number = idxReader.ReadUInt32();
|
||||
var pos = idxReader.ReadInt64();
|
||||
var length = idxReader.ReadInt32();
|
||||
|
||||
List<Tuple<ConstructorInfo, string>> types = ReadTypes<I>(tdbReader);
|
||||
Tuple<ConstructorInfo, string> objs = types[typeID];
|
||||
|
||||
var count = idxReader.ReadInt32();
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (objs == null)
|
||||
{
|
||||
var typeID = idxReader.ReadInt32();
|
||||
var number = idxReader.ReadUInt32();
|
||||
var pos = idxReader.ReadInt64();
|
||||
var length = idxReader.ReadInt32();
|
||||
|
||||
Tuple<ConstructorInfo, string> objs = types[typeID];
|
||||
|
||||
if (objs == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
T t;
|
||||
ConstructorInfo ctor = objs.Item1;
|
||||
I indexer = indexInfo.CreateIndex(number);
|
||||
|
||||
ctorArgs[0] = indexer;
|
||||
t = ctor.Invoke(ctorArgs) as T;
|
||||
|
||||
if (t != null)
|
||||
{
|
||||
entities.Add(new EntityIndex<T>(t, typeID, pos, length));
|
||||
map[indexer] = t;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
tdbReader.Close();
|
||||
T t;
|
||||
ConstructorInfo ctor = objs.Item1;
|
||||
I indexer = indexInfo.CreateIndex(number);
|
||||
|
||||
ctorArgs[0] = indexer;
|
||||
t = ctor.Invoke(ctorArgs) as T;
|
||||
|
||||
if (t != null)
|
||||
{
|
||||
entities.Add(new EntityIndex<T>(t, typeID, pos, length));
|
||||
map[indexer] = t;
|
||||
}
|
||||
}
|
||||
|
||||
tdbReader.Close();
|
||||
idxReader.Close();
|
||||
|
||||
return map;
|
||||
|
|
@ -298,7 +334,7 @@ namespace Server
|
|||
{
|
||||
var indexType = indexInfo.TypeName;
|
||||
|
||||
string dataPath = Path.Combine("Saves", indexType, $"{indexType}.bin");
|
||||
string dataPath = Path.Combine(_savePath, indexType, $"{indexType}.bin");
|
||||
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
|
|
@ -380,17 +416,13 @@ namespace Server
|
|||
WriteConsole("Loading...");
|
||||
var watch = Stopwatch.StartNew();
|
||||
|
||||
List<EntityIndex<Item>> items;
|
||||
List<EntityIndex<Mobile>> mobiles;
|
||||
List<EntityIndex<BaseGuild>> guilds;
|
||||
|
||||
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
|
||||
|
||||
Mobiles = LoadIndex(mobileIndexInfo, out mobiles);
|
||||
Items = LoadIndex(itemIndexInfo, out items);
|
||||
Guilds = LoadIndex(guildIndexInfo, out guilds);
|
||||
Mobiles = LoadIndex(mobileIndexInfo, out List<EntityIndex<Mobile>> mobiles);
|
||||
Items = LoadIndex(itemIndexInfo, out List<EntityIndex<Item>> items);
|
||||
Guilds = LoadIndex(guildIndexInfo, out List<EntityIndex<BaseGuild>> guilds);
|
||||
|
||||
LoadData(mobileIndexInfo, mobiles);
|
||||
LoadData(itemIndexInfo, items);
|
||||
|
|
@ -420,13 +452,14 @@ namespace Server
|
|||
|
||||
watch.Stop();
|
||||
|
||||
Utility.PushColor(ConsoleColor.Green);
|
||||
Console.WriteLine(
|
||||
"done ({1} items, {2} mobiles) ({0:F2} seconds)",
|
||||
watch.Elapsed.TotalSeconds,
|
||||
Items.Count,
|
||||
Mobiles.Count
|
||||
);
|
||||
|
||||
Utility.PopColor();
|
||||
|
||||
WorldState = WorldState.Running;
|
||||
}
|
||||
|
|
@ -477,33 +510,131 @@ namespace Server
|
|||
ProcessSafetyQueues();
|
||||
}
|
||||
|
||||
private static void TraceException(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var op = new StreamWriter("save-errors.log", true);
|
||||
op.WriteLine("# {0}", DateTime.UtcNow);
|
||||
|
||||
op.WriteLine(ex);
|
||||
|
||||
op.WriteLine();
|
||||
op.WriteLine();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
private static void TraceSave(params IEnumerable<KeyValuePair<string, int>>[] entityTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
var timestamp = Utility.GetTimeStamp();
|
||||
using var op = new StreamWriter("Logs/Saves/Save-Stats-{0}.log", 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
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteFiles(object state)
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
WriteConsole("Writing snapshot...");
|
||||
|
||||
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
|
||||
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
|
||||
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
|
||||
|
||||
WriteEntities(mobileIndexInfo, Mobiles, MobileTypes);
|
||||
WriteEntities(itemIndexInfo, Items, ItemTypes);
|
||||
WriteEntities(guildIndexInfo, Guilds, GuildTypes);
|
||||
Exception exception = null;
|
||||
|
||||
watch.Stop();
|
||||
Dictionary<string, int> mobileCounts = null;
|
||||
Dictionary<string, int> itemCounts = null;
|
||||
Dictionary<string, int> guildCounts = null;
|
||||
|
||||
var tempPath = Path.Combine(_tempSavePath, Utility.GetTimeStamp());
|
||||
|
||||
try
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
WriteConsole("Writing snapshot...");
|
||||
|
||||
WriteEntities(mobileIndexInfo, Mobiles, MobileTypes, tempPath, out mobileCounts);
|
||||
WriteEntities(itemIndexInfo, Items, ItemTypes, tempPath, out itemCounts);
|
||||
WriteEntities(guildIndexInfo, Guilds, GuildTypes, tempPath, out guildCounts);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
Utility.PushColor(ConsoleColor.Green);
|
||||
Console.WriteLine("done ({0:F2} seconds)", watch.Elapsed.TotalSeconds);
|
||||
Utility.PopColor();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
Utility.PushColor(ConsoleColor.Red);
|
||||
Console.WriteLine("failed");
|
||||
Utility.PopColor();
|
||||
TraceException(exception);
|
||||
|
||||
BroadcastStaff(0x35, true, "Writing world save snapshot failed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
TraceSave(mobileCounts.ToList(), itemCounts.ToList(), guildCounts.ToList());
|
||||
|
||||
EventSink.InvokeWorldSavePostSnapshot(_savePath, tempPath);
|
||||
Directory.Move(tempPath, _savePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TraceException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
m_DiskWriteHandle.Set();
|
||||
|
||||
Console.WriteLine("done ({0:F2} seconds)", watch.Elapsed.TotalSeconds);
|
||||
|
||||
Timer.DelayCall(FinishWorldSave);
|
||||
}
|
||||
|
||||
private static void WriteEntities<I, T>(IIndexInfo<I> indexInfo, Dictionary<I, T> entities, List<Type> types) where T : class, ISerializable
|
||||
private static void WriteEntities<I, T>(
|
||||
IIndexInfo<I> indexInfo,
|
||||
Dictionary<I, T> entities,
|
||||
List<Type> types,
|
||||
string savePath,
|
||||
out Dictionary<string, int> counts
|
||||
) where T : class, ISerializable
|
||||
{
|
||||
counts = new Dictionary<string, int>();
|
||||
|
||||
var typeName = indexInfo.TypeName;
|
||||
|
||||
var path = Path.Combine("Saves", typeName);
|
||||
var path = Path.Combine(savePath, typeName);
|
||||
|
||||
AssemblyHandler.EnsureDirectory(path);
|
||||
|
||||
|
|
@ -527,6 +658,12 @@ namespace Server
|
|||
e.SerializeTo(bin);
|
||||
|
||||
idx.Write((int)(bin.Position - start));
|
||||
|
||||
var type = e.GetType().FullName;
|
||||
if (type != null)
|
||||
{
|
||||
counts[type] = (counts.TryGetValue(type, out var count) ? count : 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
tdb.Write(types.Count);
|
||||
|
|
@ -569,8 +706,6 @@ namespace Server
|
|||
|
||||
WaitForWriteCompletion(); // Blocks Save until current disk flush is done.
|
||||
|
||||
++_Saves;
|
||||
|
||||
WorldState = WorldState.Saving;
|
||||
|
||||
m_DiskWriteHandle.Reset();
|
||||
|
|
@ -583,30 +718,46 @@ namespace Server
|
|||
|
||||
var watch = Stopwatch.StartNew();
|
||||
|
||||
SaveEntities(Items.Values, now);
|
||||
SaveEntities(Mobiles.Values, now);
|
||||
SaveEntities(Guilds.Values, now);
|
||||
Exception exception = null;
|
||||
|
||||
try
|
||||
{
|
||||
SaveEntities(Items.Values, now);
|
||||
SaveEntities(Mobiles.Values, now);
|
||||
SaveEntities(Guilds.Values, now);
|
||||
|
||||
EventSink.InvokeWorldSave();
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("World Save event threw an exception. Save failed!", e);
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
WorldState = WorldState.WritingSave;
|
||||
|
||||
watch.Stop();
|
||||
|
||||
var duration = watch.Elapsed.TotalSeconds;
|
||||
Console.WriteLine("done ({0:F2} seconds)", duration);
|
||||
|
||||
// Only broadcast if it took at least 150ms
|
||||
if (duration >= 0.15)
|
||||
if (exception == null)
|
||||
{
|
||||
Broadcast(0x35, true, $"World Save completed in {duration:F2} seconds.");
|
||||
var duration = watch.Elapsed.TotalSeconds;
|
||||
Utility.PushColor(ConsoleColor.Green);
|
||||
Console.WriteLine("done ({0:F2} seconds)", duration);
|
||||
Utility.PopColor();
|
||||
|
||||
// Only broadcast if it took at least 150ms
|
||||
if (duration >= 0.15)
|
||||
{
|
||||
Broadcast(0x35, true, $"World Save completed in {duration:F2} seconds.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Utility.PushColor(ConsoleColor.Red);
|
||||
Console.WriteLine("failed");
|
||||
Utility.PopColor();
|
||||
TraceException(exception);
|
||||
|
||||
BroadcastStaff(0x35, true, "World save failed.");
|
||||
}
|
||||
|
||||
ThreadPool.QueueUserWorkItem(WriteFiles);
|
||||
|
|
@ -662,6 +813,7 @@ namespace Server
|
|||
public static Mobile FindMobile(Serial serial, bool returnDeleted = false) =>
|
||||
FindEntity<Mobile>(serial, returnDeleted);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BaseGuild FindGuild(Serial serial) => Guilds.TryGetValue(serial, out var guild) ? guild : null;
|
||||
|
||||
public static void AddEntity<T>(T entity) where T : class, IEntity
|
||||
|
|
@ -705,6 +857,7 @@ namespace Server
|
|||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void AddGuild(BaseGuild guild) => Guilds[guild.Serial] = guild;
|
||||
|
||||
public static void RemoveEntity<T>(T entity) where T : class, IEntity
|
||||
|
|
@ -743,6 +896,7 @@ namespace Server
|
|||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void RemoveGuild(BaseGuild guild) => Guilds.Remove(guild.Serial);
|
||||
|
||||
private static void SerializeTo(this ISerializable entity, IGenericWriter writer)
|
||||
|
|
|
|||
33
Projects/Server/World/WorldEvents.cs
Normal file
33
Projects/Server/World/WorldEvents.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
public class WorldSavePostSnapshotEventArgs
|
||||
{
|
||||
public string OldSavePath { get; }
|
||||
public string NewSavePath { get; }
|
||||
|
||||
public WorldSavePostSnapshotEventArgs(string oldSavePath, string newSavePath)
|
||||
{
|
||||
OldSavePath = oldSavePath;
|
||||
NewSavePath = newSavePath;
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class EventSink
|
||||
{
|
||||
public static event Action WorldLoad;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeWorldLoad() => WorldLoad?.Invoke();
|
||||
|
||||
public static event Action WorldSave;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeWorldSave() => WorldSave?.Invoke();
|
||||
|
||||
public static event Action<WorldSavePostSnapshotEventArgs> WorldSavePostSnapshot;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeWorldSavePostSnapshot(string oldSavePath, string newSavePath) =>
|
||||
WorldSavePostSnapshot?.Invoke(new WorldSavePostSnapshotEventArgs(oldSavePath, newSavePath));
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +56,9 @@ namespace Server.Configurations
|
|||
throw new JsonException($"Failed to deserialize {path}.");
|
||||
}
|
||||
|
||||
Utility.PushColor(ConsoleColor.Green);
|
||||
Console.WriteLine("done");
|
||||
Utility.PopColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,25 +5,27 @@ namespace Server.Misc
|
|||
{
|
||||
public class AutoSave : Timer
|
||||
{
|
||||
private static readonly TimeSpan m_Delay = TimeSpan.FromMinutes(5.0);
|
||||
private static readonly TimeSpan m_Warning = TimeSpan.Zero;
|
||||
public static TimeSpan Delay { get; private set; }
|
||||
public static TimeSpan Warning { get; private set; }
|
||||
public static string BackupPath { get; private set; }
|
||||
|
||||
private static readonly string[] m_Backups =
|
||||
{
|
||||
"Third Backup",
|
||||
"Second Backup",
|
||||
"Most Recent"
|
||||
};
|
||||
|
||||
public AutoSave() : base(m_Delay - m_Warning, m_Delay) => Priority = TimerPriority.OneMinute;
|
||||
public AutoSave() : base(Delay - Warning, Delay) => Priority = TimerPriority.OneMinute;
|
||||
|
||||
public static bool SavesEnabled { get; set; } = true;
|
||||
// private static TimeSpan m_Warning = TimeSpan.FromSeconds( 15.0 );
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
BackupPath = ServerConfiguration.GetOrUpdateSetting("autosave.backupPath", "Backups/Automatic");
|
||||
Delay = ServerConfiguration.GetOrUpdateSetting("autosave.saveDelay", TimeSpan.FromMinutes(5.0));
|
||||
Warning = ServerConfiguration.GetOrUpdateSetting("autosave.warningDelay", TimeSpan.Zero);
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
new AutoSave().Start();
|
||||
CommandSystem.Register("SetSaves", AccessLevel.Administrator, SetSaves_OnCommand);
|
||||
|
||||
EventSink.WorldSavePostSnapshot += Backup;
|
||||
}
|
||||
|
||||
[Usage("SetSaves <true | false>"), Description("Enables or disables automatic shard saving.")]
|
||||
|
|
@ -47,13 +49,13 @@ namespace Server.Misc
|
|||
return;
|
||||
}
|
||||
|
||||
if (m_Warning == TimeSpan.Zero)
|
||||
if (Warning == TimeSpan.Zero)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
else
|
||||
{
|
||||
var s = (int)m_Warning.TotalSeconds;
|
||||
var s = (int)Warning.TotalSeconds;
|
||||
var m = s / 60;
|
||||
s %= 60;
|
||||
|
||||
|
|
@ -78,130 +80,25 @@ namespace Server.Misc
|
|||
World.Broadcast(0x35, true, "The world will save in {0} second{1}.", s, s != 1 ? "s" : "");
|
||||
}
|
||||
|
||||
DelayCall(m_Warning, Save);
|
||||
DelayCall(Warning, Save);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
if (AutoRestart.Restarting || World.WorldState != WorldState.Running)
|
||||
if (!AutoRestart.Restarting && World.WorldState == WorldState.Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
World.WaitForWriteCompletion();
|
||||
|
||||
try
|
||||
{
|
||||
Backup();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("WARNING: Automatic backup FAILED: {0}", e);
|
||||
}
|
||||
|
||||
World.Save();
|
||||
}
|
||||
|
||||
private static void Backup()
|
||||
{
|
||||
if (m_Backups.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var root = Path.Combine(Core.BaseDirectory, "Backups/Automatic");
|
||||
|
||||
AssemblyHandler.EnsureDirectory(root);
|
||||
|
||||
var existing = Directory.GetDirectories(root);
|
||||
|
||||
for (var i = 0; i < m_Backups.Length; ++i)
|
||||
{
|
||||
var dir = Match(existing, m_Backups[i]);
|
||||
|
||||
if (dir == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
var timeStamp = FindTimeStamp(dir.Name);
|
||||
|
||||
if (timeStamp != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
dir.MoveTo(FormatDirectory(root, m_Backups[i - 1], timeStamp));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
dir.Delete(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var saves = Path.Combine(Core.BaseDirectory, "Saves");
|
||||
|
||||
if (Directory.Exists(saves))
|
||||
{
|
||||
Directory.Move(saves, FormatDirectory(root, m_Backups[^1], GetTimeStamp()));
|
||||
World.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static DirectoryInfo Match(string[] paths, string match)
|
||||
private static void Backup(WorldSavePostSnapshotEventArgs args)
|
||||
{
|
||||
for (var i = 0; i < paths.Length; ++i)
|
||||
{
|
||||
var info = new DirectoryInfo(paths[i]);
|
||||
var backupPath = Path.Combine(BackupPath, Utility.GetTimeStamp());
|
||||
AssemblyHandler.EnsureDirectory(BackupPath);
|
||||
Directory.Move(args.OldSavePath, backupPath);
|
||||
|
||||
if (info.Name.StartsWithOrdinal(match))
|
||||
{
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FormatDirectory(string root, string name, string timeStamp) =>
|
||||
Path.Combine(root, $"{name} ({timeStamp})");
|
||||
|
||||
private static string FindTimeStamp(string input)
|
||||
{
|
||||
var start = input.IndexOfOrdinal('(');
|
||||
|
||||
if (start >= 0)
|
||||
{
|
||||
var end = input.IndexOf(')', ++start);
|
||||
|
||||
if (end >= start)
|
||||
{
|
||||
return input.Substring(start, end - start);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetTimeStamp()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return $"{now.Day}-{now.Month}-{now.Year} {now.Hour}-{now.Minute:D2}-{now.Second:D2}";
|
||||
Console.WriteLine("AutoSave: Created backup at {0}", backupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ namespace Server.Misc
|
|||
|
||||
try
|
||||
{
|
||||
var timeStamp = GetTimeStamp();
|
||||
var timeStamp = Utility.GetTimeStamp();
|
||||
|
||||
var root = Core.BaseDirectory;
|
||||
var rootBackup = Path.Combine(root, $"Backups/Crashed/{timeStamp}/");
|
||||
|
|
@ -137,7 +137,7 @@ namespace Server.Misc
|
|||
|
||||
try
|
||||
{
|
||||
var timeStamp = GetTimeStamp();
|
||||
var timeStamp = Utility.GetTimeStamp();
|
||||
var fileName = $"Crash {timeStamp}.log";
|
||||
|
||||
var root = Core.BaseDirectory;
|
||||
|
|
@ -219,12 +219,5 @@ namespace Server.Misc
|
|||
Console.WriteLine("failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTimeStamp()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return $"{now.Day}-{now.Month}-{now.Year}-{now.Hour}-{now.Minute}-{now.Second}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue