diff --git a/Projects/Server.Tests/Fixtures/ServerFixture.cs b/Projects/Server.Tests/Fixtures/ServerFixture.cs index bf3da700a..48d7b369a 100644 --- a/Projects/Server.Tests/Fixtures/ServerFixture.cs +++ b/Projects/Server.Tests/Fixtures/ServerFixture.cs @@ -15,6 +15,9 @@ namespace Server.Tests // Configure / Initialize TestMapDefinitions.ConfigureTestMapDefinitions(); + // Configure the world + World.Configure(); + // Load the world World.Load(); } diff --git a/Projects/Server/Configuration/ServerConfiguration.cs b/Projects/Server/Configuration/ServerConfiguration.cs index 9a0739445..c5e91f83e 100644 --- a/Projects/Server/Configuration/ServerConfiguration.cs +++ b/Projects/Server/Configuration/ServerConfiguration.cs @@ -179,7 +179,9 @@ namespace Server throw new FileNotFoundException($"Failed to deserialize {m_FilePath}."); } + Utility.PushColor(ConsoleColor.Green); Console.WriteLine("done"); + Utility.PopColor(); } else { diff --git a/Projects/Server/Events/AccountLoginEvent.cs b/Projects/Server/Events/AccountLoginEvent.cs index 7d2739cc3..cfe5636a3 100644 --- a/Projects/Server/Events/AccountLoginEvent.cs +++ b/Projects/Server/Events/AccountLoginEvent.cs @@ -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 AccountLogin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeAccountLogin(AccountLoginEventArgs e) => AccountLogin?.Invoke(e); } } diff --git a/Projects/Server/Events/AggressiveActionEvent.cs b/Projects/Server/Events/AggressiveActionEvent.cs index 8370c7100..663ddd8bf 100644 --- a/Projects/Server/Events/AggressiveActionEvent.cs +++ b/Projects/Server/Events/AggressiveActionEvent.cs @@ -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 AggressiveAction; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeAggressiveAction(AggressiveActionEventArgs e) => AggressiveAction?.Invoke(e); } } diff --git a/Projects/Server/Events/CharacterCreatedEvent.cs b/Projects/Server/Events/CharacterCreatedEvent.cs index c11251dd1..b919193ea 100644 --- a/Projects/Server/Events/CharacterCreatedEvent.cs +++ b/Projects/Server/Events/CharacterCreatedEvent.cs @@ -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 CharacterCreated; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeCharacterCreated(CharacterCreatedEventArgs e) => CharacterCreated?.Invoke(e); } } diff --git a/Projects/Server/Events/EventSink.cs b/Projects/Server/Events/EventSink.cs index 66193a222..797750fc1 100644 --- a/Projects/Server/Events/EventSink.cs +++ b/Projects/Server/Events/EventSink.cs @@ -106,12 +106,6 @@ namespace Server public static event Action 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 SetAbility; public static void InvokeSetAbility(Mobile mobile, int index) => SetAbility?.Invoke(mobile, index); diff --git a/Projects/Server/Events/FastwalkEvent.cs b/Projects/Server/Events/FastwalkEvent.cs index 1237d0b19..4ef3bd5d8 100644 --- a/Projects/Server/Events/FastwalkEvent.cs +++ b/Projects/Server/Events/FastwalkEvent.cs @@ -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 FastWalk; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeFastWalk(FastWalkEventArgs e) => FastWalk?.Invoke(e); } } diff --git a/Projects/Server/Events/GameLoginEvent.cs b/Projects/Server/Events/GameLoginEvent.cs index 823c0f6e2..03a67c0f4 100644 --- a/Projects/Server/Events/GameLoginEvent.cs +++ b/Projects/Server/Events/GameLoginEvent.cs @@ -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 GameLogin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeGameLogin(GameLoginEventArgs e) => GameLogin?.Invoke(e); } } diff --git a/Projects/Server/Events/MovementEvent.cs b/Projects/Server/Events/MovementEvent.cs index 035488087..b3125f80a 100644 --- a/Projects/Server/Events/MovementEvent.cs +++ b/Projects/Server/Events/MovementEvent.cs @@ -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 Movement; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeMovement(MovementEventArgs e) => Movement?.Invoke(e); } } diff --git a/Projects/Server/Events/ServerCrashedEvent.cs b/Projects/Server/Events/ServerCrashedEvent.cs index 5667cdd8b..ee4445c58 100644 --- a/Projects/Server/Events/ServerCrashedEvent.cs +++ b/Projects/Server/Events/ServerCrashedEvent.cs @@ -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 ServerCrashed; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeServerCrashed(ServerCrashedEventArgs e) => ServerCrashed?.Invoke(e); } } diff --git a/Projects/Server/Events/ServerListEvent.cs b/Projects/Server/Events/ServerListEvent.cs index 7bf5a7591..c646a65a2 100644 --- a/Projects/Server/Events/ServerListEvent.cs +++ b/Projects/Server/Events/ServerListEvent.cs @@ -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 ServerList; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeServerList(ServerListEventArgs e) => ServerList?.Invoke(e); } } diff --git a/Projects/Server/Events/SocketConnectionEvent.cs b/Projects/Server/Events/SocketConnectionEvent.cs index 365fd3ac8..c39756a99 100644 --- a/Projects/Server/Events/SocketConnectionEvent.cs +++ b/Projects/Server/Events/SocketConnectionEvent.cs @@ -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 SocketConnect; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeSocketConnect(SocketConnectEventArgs e) => SocketConnect?.Invoke(e); } } diff --git a/Projects/Server/Events/SpeechEvent.cs b/Projects/Server/Events/SpeechEvent.cs index f309c6ead..0c17dff18 100644 --- a/Projects/Server/Events/SpeechEvent.cs +++ b/Projects/Server/Events/SpeechEvent.cs @@ -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 Speech; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeSpeech(SpeechEventArgs e) => Speech?.Invoke(e); } } diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index bc24cc008..4935a8ed4 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -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}"; + } } } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index dfa0093a4..c2f7101ca 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -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 _pendingDelete = new(); private static readonly ConcurrentQueue _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 ItemTypes { get; } = new(); internal static List MobileTypes { get; } = new(); internal static List GuildTypes { get; } = new(); @@ -132,6 +135,15 @@ namespace Server public static Dictionary Items { get; private set; } public static Dictionary 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 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> ReadTypes(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>(); @@ -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> types = ReadTypes(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> types = ReadTypes(tdbReader); + Tuple 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 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, 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, 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> items; - List> mobiles; - List> guilds; - IIndexInfo itemIndexInfo = new EntityTypeIndex("Items"); IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - Mobiles = LoadIndex(mobileIndexInfo, out mobiles); - Items = LoadIndex(itemIndexInfo, out items); - Guilds = LoadIndex(guildIndexInfo, out guilds); + Mobiles = LoadIndex(mobileIndexInfo, out List> mobiles); + Items = LoadIndex(itemIndexInfo, out List> items); + Guilds = LoadIndex(guildIndexInfo, out List> 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>[] 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 itemIndexInfo = new EntityTypeIndex("Items"); IIndexInfo mobileIndexInfo = new EntityTypeIndex("Mobiles"); IIndexInfo guildIndexInfo = new EntityTypeIndex("Guilds"); - WriteEntities(mobileIndexInfo, Mobiles, MobileTypes); - WriteEntities(itemIndexInfo, Items, ItemTypes); - WriteEntities(guildIndexInfo, Guilds, GuildTypes); + Exception exception = null; - watch.Stop(); + Dictionary mobileCounts = null; + Dictionary itemCounts = null; + Dictionary 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(IIndexInfo indexInfo, Dictionary entities, List types) where T : class, ISerializable + private static void WriteEntities( + IIndexInfo indexInfo, + Dictionary entities, + List types, + string savePath, + out Dictionary counts + ) where T : class, ISerializable { + counts = new Dictionary(); + 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(serial, returnDeleted); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static BaseGuild FindGuild(Serial serial) => Guilds.TryGetValue(serial, out var guild) ? guild : null; public static void AddEntity(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 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) diff --git a/Projects/Server/World/WorldEvents.cs b/Projects/Server/World/WorldEvents.cs new file mode 100644 index 000000000..66e8b07e5 --- /dev/null +++ b/Projects/Server/World/WorldEvents.cs @@ -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 WorldSavePostSnapshot; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvokeWorldSavePostSnapshot(string oldSavePath, string newSavePath) => + WorldSavePostSnapshot?.Invoke(new WorldSavePostSnapshotEventArgs(oldSavePath, newSavePath)); + } +} diff --git a/Projects/UOContent/Configuration/EmailConfiguration.cs b/Projects/UOContent/Configuration/EmailConfiguration.cs index 32bce55ce..a2eeeb9d7 100644 --- a/Projects/UOContent/Configuration/EmailConfiguration.cs +++ b/Projects/UOContent/Configuration/EmailConfiguration.cs @@ -56,7 +56,9 @@ namespace Server.Configurations throw new JsonException($"Failed to deserialize {path}."); } + Utility.PushColor(ConsoleColor.Green); Console.WriteLine("done"); + Utility.PopColor(); } else { diff --git a/Projects/UOContent/Misc/AutoSave.cs b/Projects/UOContent/Misc/AutoSave.cs index 001397743..aa585386d 100644 --- a/Projects/UOContent/Misc/AutoSave.cs +++ b/Projects/UOContent/Misc/AutoSave.cs @@ -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 "), 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); } } } diff --git a/Projects/UOContent/Misc/CrashGuard.cs b/Projects/UOContent/Misc/CrashGuard.cs index 5d28803a7..6cf959666 100644 --- a/Projects/UOContent/Misc/CrashGuard.cs +++ b/Projects/UOContent/Misc/CrashGuard.cs @@ -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}"; - } } }