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:
Kamron Batman 2021-02-20 19:32:08 -08:00 committed by GitHub
parent 6f5312cb89
commit da01f5b8a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 325 additions and 210 deletions

View file

@ -15,6 +15,9 @@ namespace Server.Tests
// Configure / Initialize
TestMapDefinitions.ConfigureTestMapDefinitions();
// Configure the world
World.Configure();
// Load the world
World.Load();
}

View file

@ -179,7 +179,9 @@ namespace Server
throw new FileNotFoundException($"Failed to deserialize {m_FilePath}.");
}
Utility.PushColor(ConsoleColor.Green);
Console.WriteLine("done");
Utility.PopColor();
}
else
{

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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}";
}
}
}

View file

@ -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)

View 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));
}
}

View file

@ -56,7 +56,9 @@ namespace Server.Configurations
throw new JsonException($"Failed to deserialize {path}.");
}
Utility.PushColor(ConsoleColor.Green);
Console.WriteLine("done");
Utility.PopColor();
}
else
{

View file

@ -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);
}
}
}

View file

@ -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}";
}
}
}