From d07caea0f6bea49fe91c2e6aa16cbe693ecd4d4c Mon Sep 17 00:00:00 2001 From: Westin Miller Date: Sat, 2 Dec 2017 21:19:09 -0800 Subject: [PATCH] New saving system with proper waiting for file flush Command context for servers now supports Respond() Chat manager now treats the steam ID Sync.MyId as a local destination, and processes the event accordingly. Save makes better use of Task<> Restart actually waits for save PlayerCollectionExtension uses a dictionary lookup for TryGetBySteamId Shutting the UI window properly closes Torch Torch Dispose renamed to Destroy, VRage Dispose marked as obsolete (do not use) --- Torch.API/ITorchBase.cs | 16 ++- Torch.API/Session/GameSaveResult.cs | 44 +++++++ Torch.API/Torch.API.csproj | 1 + Torch.Client/TorchClient.cs | 4 +- Torch.Server/Initializer.cs | 25 ++-- Torch.Server/TorchServer.cs | 64 +++------- Torch.Server/Views/ChatControl.xaml.cs | 4 +- Torch/Commands/CommandContext.cs | 30 +++-- Torch/Commands/CommandManager.cs | 12 +- Torch/Commands/TorchCommands.cs | 71 ++++++++++-- .../MyPlayerCollectionExtensions.cs | 10 +- .../Managers/ChatManager/ChatManagerClient.cs | 18 +-- .../Managers/ChatManager/ChatManagerServer.cs | 11 +- Torch/Patches/TorchAsyncSaving.cs | 109 ++++++++++++++++++ Torch/SaveGameStatus.cs | 28 ----- Torch/Torch.csproj | 1 + Torch/TorchBase.cs | 66 ++++++----- 17 files changed, 348 insertions(+), 166 deletions(-) create mode 100644 Torch.API/Session/GameSaveResult.cs create mode 100644 Torch/Patches/TorchAsyncSaving.cs delete mode 100644 Torch/SaveGameStatus.cs diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs index a89534c..3141edd 100644 --- a/Torch.API/ITorchBase.cs +++ b/Torch.API/ITorchBase.cs @@ -18,21 +18,25 @@ namespace Torch.API /// /// Fired when the session begins loading. /// + [Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")] event Action SessionLoading; - + /// /// Fired when the session finishes loading. /// + [Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")] event Action SessionLoaded; /// /// Fires when the session begins unloading. /// + [Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")] event Action SessionUnloading; /// /// Fired when the session finishes unloading. /// + [Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")] event Action SessionUnloaded; /// @@ -90,15 +94,17 @@ namespace Torch.API void Stop(); /// - /// Restart the Torch instance. + /// Restart the Torch instance, blocking until the restart has been performed. /// void Restart(); /// /// Initializes a save of the game. /// - /// Id of the player who initiated the save. - Task Save(long callerId); + /// timeout before the save is treated as failed, or -1 for no timeout + /// Only start saving if we aren't already saving + /// Future result of the save, or null if one is in progress and in exclusive mode + Task Save(int timeoutMs = -1, bool exclusive = false); /// /// Initialize the Torch instance. Before this is invalid. @@ -108,7 +114,7 @@ namespace Torch.API /// /// Disposes the Torch instance. After this is invalid. /// - void Dispose(); + void Destroy(); /// /// The current state of the game this instance of torch is controlling. diff --git a/Torch.API/Session/GameSaveResult.cs b/Torch.API/Session/GameSaveResult.cs new file mode 100644 index 0000000..51d16b8 --- /dev/null +++ b/Torch.API/Session/GameSaveResult.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.API.Session +{ + /// + /// The result of a save operation + /// + public enum GameSaveResult + { + /// + /// Successfully saved + /// + Success = 0, + + /// + /// The game wasn't ready to be saved + /// + GameNotReady = -1, + + /// + /// Failed to take the snapshot of the current world state + /// + FailedToTakeSnapshot = -2, + + /// + /// Failed to save the snapshot to disk + /// + FailedToSaveToDisk = -3, + + /// + /// An unknown error occurred + /// + UnknownError = -4, + + /// + /// The save operation timed out + /// + TimedOut = -5 + } +} \ No newline at end of file diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index bd5e5f4..48001e8 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -187,6 +187,7 @@ + diff --git a/Torch.Client/TorchClient.cs b/Torch.Client/TorchClient.cs index 22d2d15..00f3ab5 100644 --- a/Torch.Client/TorchClient.cs +++ b/Torch.Client/TorchClient.cs @@ -66,9 +66,9 @@ namespace Torch.Client SetRenderWindowTitle($"Space Engineers v{GameVersion} with Torch v{TorchVersion}"); } - public override void Dispose() + public override void Destroy() { - base.Dispose(); + base.Destroy(); _startup.DetectSharpDxLeaksAfterRun(); } diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index 020171f..d8a07a1 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -84,17 +84,26 @@ quit"; public void Run() { _server = new TorchServer(_config); - _server.Init(); - - if (!_config.NoGui) + try { - var ui = new TorchUI(_server); - if (_config.Autostart) + _server.Init(); + + if (!_config.NoGui) + { + var ui = new TorchUI(_server); + if (_config.Autostart) + _server.Start(); + ui.ShowDialog(); + } + else _server.Start(); - ui.ShowDialog(); } - else - _server.Start(); + finally + { + if (_server.IsRunning) + _server.Stop(); + _server.Destroy(); + } } private TorchConfig InitConfig() diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index aad2fc5..7d18a50 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -178,16 +178,22 @@ namespace Torch.Server /// public override void Restart() { - Save(0).Wait(); - Stop(); - LogManager.Flush(); + Save().ContinueWith((task, torchO) => + { + var torch = (TorchServer) torchO; + torch.Stop(); + // TODO clone this + var config = (TorchConfig) torch.Config; + LogManager.Flush(); - var exe = Assembly.GetExecutingAssembly().Location; - ((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString(); - Config.Autostart = true; - Process.Start(exe, Config.ToString()); + string exe = Assembly.GetExecutingAssembly().Location; + Debug.Assert(exe != null); + config.WaitForPID = Process.GetCurrentProcess().Id.ToString(); + config.Autostart = true; + Process.Start(exe, config.ToString()); - Process.GetCurrentProcess().Kill(); + Process.GetCurrentProcess().Kill(); + }, this, TaskContinuationOptions.RunContinuationsAsynchronously); } /// @@ -295,47 +301,5 @@ namespace Torch.Server } #endregion - - /// - public override Task Save(long callerId) - { - return SaveGameAsync(statusCode => SaveCompleted(statusCode, callerId)); - } - - /// - /// Callback for when save has finished. - /// - /// Return code of the save operation - /// Caller of the save operation - private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0) - { - string response = null; - switch (statusCode) - { - case SaveGameStatus.Success: - Log.Info("Save completed."); - response = "Saved game."; - break; - case SaveGameStatus.SaveInProgress: - Log.Error("Save failed, a save is already in progress."); - response = "Save failed, a save is already in progress."; - break; - case SaveGameStatus.GameNotReady: - Log.Error("Save failed, game was not ready."); - response = "Save failed, game was not ready."; - break; - case SaveGameStatus.TimedOut: - Log.Error("Save failed, save timed out."); - response = "Save failed, save timed out."; - break; - default: - break; - } - if (MySession.Static.Players.TryGetPlayerId(callerId, out MyPlayer.PlayerId result)) - { - Managers.GetManager()?.SendMessageAsOther("Server", response, - statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId); - } - } } } \ No newline at end of file diff --git a/Torch.Server/Views/ChatControl.xaml.cs b/Torch.Server/Views/ChatControl.xaml.cs index a6c1cb7..d88d8e4 100644 --- a/Torch.Server/Views/ChatControl.xaml.cs +++ b/Torch.Server/Views/ChatControl.xaml.cs @@ -139,9 +139,7 @@ namespace Torch.Server InsertMessage(new TorchChatMessage("Server", text, MyFontEnum.DarkBlue)); _server.Invoke(() => { - string response = commands.HandleCommandFromServer(text); - if (!string.IsNullOrWhiteSpace(response)) - InsertMessage(new TorchChatMessage("Server", response, MyFontEnum.Blue)); + commands.HandleCommandFromServer(text); }); } else diff --git a/Torch/Commands/CommandContext.cs b/Torch/Commands/CommandContext.cs index 30700a7..15797a3 100644 --- a/Torch/Commands/CommandContext.cs +++ b/Torch/Commands/CommandContext.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Sandbox.Engine.Networking; +using Sandbox.Game.Multiplayer; using Torch.API; using Torch.API.Managers; using Torch.API.Plugins; @@ -15,40 +17,48 @@ namespace Torch.Commands /// The plugin that added this command. /// public ITorchPlugin Plugin { get; } + /// /// The current Torch instance. /// public ITorchBase Torch { get; } + /// - /// The player who ran the command. + /// The player who ran the command, or null if the server sent it. /// - public IMyPlayer Player { get; } + public IMyPlayer Player => Torch.CurrentSession.KeenSession.Players.TryGetPlayerBySteamId(_steamIdSender); + + /// + /// Was this message sent by this program. + /// + public bool SentBySelf => _steamIdSender == Sync.MyId; + + private ulong _steamIdSender; + /// /// The command arguments split by spaces and quotes. Ex. "this is" a command -> {this is, a, command} /// public List Args { get; } + /// /// The non-split argument string. /// public string RawArgs { get; } - public string Response { get; private set; } - - public CommandContext(ITorchBase torch, ITorchPlugin plugin, IMyPlayer player, string rawArgs = null, List args = null) + public CommandContext(ITorchBase torch, ITorchPlugin plugin, ulong steamIdSender, string rawArgs = null, + List args = null) { Torch = torch; Plugin = plugin; - Player = player; + _steamIdSender = steamIdSender; RawArgs = rawArgs; Args = args ?? new List(); } public void Respond(string message, string sender = "Server", string font = MyFontEnum.Blue) { - Response = message; - - if (Player != null) - Torch.CurrentSession.Managers.GetManager()?.SendMessageAsOther(sender, message, font, Player.SteamUserId); + Torch.CurrentSession.Managers.GetManager() + ?.SendMessageAsOther(sender, message, font, _steamIdSender); } } } \ No newline at end of file diff --git a/Torch/Commands/CommandManager.cs b/Torch/Commands/CommandManager.cs index fa63d61..cd52732 100644 --- a/Torch/Commands/CommandManager.cs +++ b/Torch/Commands/CommandManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using NLog; +using Sandbox.Game.Multiplayer; using Sandbox.Game.World; using Torch.API; using Torch.API.Managers; @@ -80,23 +81,22 @@ namespace Torch.Commands } } - public string HandleCommandFromServer(string message) + public bool HandleCommandFromServer(string message) { var cmdText = new string(message.Skip(1).ToArray()); var command = Commands.GetCommand(cmdText, out string argText); if (command == null) - return null; + return false; var cmdPath = string.Join(".", command.Path); var splitArgs = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast().Select(x => x.ToString().Replace("\"", "")).ToList(); _log.Trace($"Invoking {cmdPath} for server."); - var context = new CommandContext(Torch, command.Plugin, null, argText, splitArgs); + var context = new CommandContext(Torch, command.Plugin, Sync.MyId, argText, splitArgs); if (command.TryInvoke(context)) _log.Info($"Server ran command '{message}'"); else context.Respond($"Invalid Syntax: {command.SyntaxHelp}"); - - return context.Response; + return true; } public void HandleCommand(TorchChatMessage msg, ref bool consumed) @@ -136,7 +136,7 @@ namespace Torch.Commands var splitArgs = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast().Select(x => x.ToString().Replace("\"", "")).ToList(); _log.Trace($"Invoking {cmdPath} for player {player.DisplayName}"); - var context = new CommandContext(Torch, command.Plugin, player, argText, splitArgs); + var context = new CommandContext(Torch, command.Plugin, steamId, argText, splitArgs); Torch.Invoke(() => { if (command.TryInvoke(context)) diff --git a/Torch/Commands/TorchCommands.cs b/Torch/Commands/TorchCommands.cs index 0b7b24e..469a00e 100644 --- a/Torch/Commands/TorchCommands.cs +++ b/Torch/Commands/TorchCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; @@ -8,10 +9,13 @@ using System.Threading.Tasks; using System.Timers; using Sandbox.ModAPI; using Torch; +using Torch.API; using Torch.API.Managers; +using Torch.API.Session; using Torch.Commands.Permissions; using Torch.Managers; using VRage.Game.ModAPI; +using Log = NLog.Fluent.Log; namespace Torch.Commands { @@ -49,7 +53,8 @@ namespace Torch.Commands } else { - Context.Respond($"Use the {commandManager.Prefix}longhelp command and check your Comms menu for a full list of commands."); + Context.Respond( + $"Use the {commandManager.Prefix}longhelp command and check your Comms menu for a full list of commands."); } } @@ -106,7 +111,8 @@ namespace Torch.Commands [Permission(MyPromoteLevel.None)] public void Plugins() { - var plugins = Context.Torch.Managers.GetManager()?.Plugins.Select(p => p.Value.Name) ?? Enumerable.Empty(); + var plugins = Context.Torch.Managers.GetManager()?.Plugins.Select(p => p.Value.Name) ?? + Enumerable.Empty(); Context.Respond($"Loaded plugins: {string.Join(", ", plugins)}"); } @@ -115,8 +121,14 @@ namespace Torch.Commands { Context.Respond("Stopping server."); if (save) - Context.Torch.Save(Context.Player?.IdentityId ?? 0).Wait(); - Context.Torch.Stop(); + DoSave()?.ContinueWith((a, mod) => + { + ITorchBase torch = (mod as CommandModule)?.Context?.Torch; + Debug.Assert(torch != null); + torch.Stop(); + }, this, TaskContinuationOptions.RunContinuationsAsynchronously); + else + Context.Torch.Stop(); } [Command("restart", "Restarts the server.")] @@ -138,21 +150,20 @@ namespace Torch.Commands { if (i >= 60 && i % 60 == 0) { - Context.Torch.CurrentSession.Managers.GetManager().SendMessageAsSelf($"Restarting server in {i / 60} minute{Pluralize(i / 60)}."); + Context.Torch.CurrentSession.Managers.GetManager() + .SendMessageAsSelf($"Restarting server in {i / 60} minute{Pluralize(i / 60)}."); yield return null; } else if (i > 0) { if (i < 11) - Context.Torch.CurrentSession.Managers.GetManager().SendMessageAsSelf($"Restarting server in {i} second{Pluralize(i)}."); + Context.Torch.CurrentSession.Managers.GetManager() + .SendMessageAsSelf($"Restarting server in {i} second{Pluralize(i)}."); yield return null; } else { - Context.Torch.Invoke(() => - { - Context.Torch.Restart(); - }); + Context.Torch.Restart(); yield break; } } @@ -171,7 +182,45 @@ namespace Torch.Commands public void Save() { Context.Respond("Saving game."); - Context.Torch.Save(Context.Player?.IdentityId ?? 0); + DoSave(); + } + + private Task DoSave() + { + Task task = Context.Torch.Save(60 * 1000, true); + if (task == null) + { + Context.Respond("Save failed, a save is already in progress"); + return null; + } + return task.ContinueWith((taskCapture, state) => + { + CommandContext context = (state as CommandModule)?.Context; + Debug.Assert(context != null); + switch (taskCapture.Result) + { + case GameSaveResult.Success: + context.Respond("Saved game."); + break; + case GameSaveResult.GameNotReady: + context.Respond("Save failed: Game was not ready."); + break; + case GameSaveResult.TimedOut: + context.Respond("Save failed: Save timed out."); + break; + case GameSaveResult.FailedToTakeSnapshot: + context.Respond("Save failed: unable to take snapshot"); + break; + case GameSaveResult.FailedToSaveToDisk: + context.Respond("Save failed: unable to save to disk"); + break; + case GameSaveResult.UnknownError: + context.Respond("Save failed: unknown reason"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + }, this, TaskContinuationOptions.RunContinuationsAsynchronously); } } } \ No newline at end of file diff --git a/Torch/Extensions/MyPlayerCollectionExtensions.cs b/Torch/Extensions/MyPlayerCollectionExtensions.cs index 05a37d1..aa9bf39 100644 --- a/Torch/Extensions/MyPlayerCollectionExtensions.cs +++ b/Torch/Extensions/MyPlayerCollectionExtensions.cs @@ -10,10 +10,14 @@ namespace Torch { public static class MyPlayerCollectionExtensions { - public static MyPlayer TryGetPlayerBySteamId(this MyPlayerCollection collection, ulong steamId) + public static MyPlayer TryGetPlayerBySteamId(this MyPlayerCollection collection, ulong steamId, int serialId = 0) { - var id = collection.GetAllPlayers().FirstOrDefault(x => x.SteamId == steamId); - return id == default(MyPlayer.PlayerId) ? null : collection.GetPlayerById(id); + long identity = collection.TryGetIdentityId(steamId, serialId); + if (identity == 0) + return null; + if (!collection.TryGetPlayerId(identity, out MyPlayer.PlayerId playerId)) + return null; + return collection.TryGetPlayerById(playerId, out MyPlayer player) ? player : null; } } } diff --git a/Torch/Managers/ChatManager/ChatManagerClient.cs b/Torch/Managers/ChatManager/ChatManagerClient.cs index f435b77..0ded357 100644 --- a/Torch/Managers/ChatManager/ChatManagerClient.cs +++ b/Torch/Managers/ChatManager/ChatManagerClient.cs @@ -114,8 +114,7 @@ namespace Torch.Managers.ChatManager if (!sendToOthers) return; var torchMsg = new TorchChatMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", Sync.MyId, messageText); - var consumed = false; - MessageRecieved?.Invoke(torchMsg, ref consumed); + bool consumed = RaiseMessageRecieved(torchMsg); if (!consumed) consumed = OfflineMessageProcessor(torchMsg); sendToOthers = !consumed; @@ -135,21 +134,24 @@ namespace Torch.Managers.ChatManager { var torchMsg = new TorchChatMessage(steamUserId, message, (steamUserId == MyGameService.UserId) ? MyFontEnum.DarkBlue : MyFontEnum.Blue); - var consumed = false; - MessageRecieved?.Invoke(torchMsg, ref consumed); - if (!consumed && HasHud) + if (!RaiseMessageRecieved(torchMsg) && HasHud) _hudChatMessageReceived.Invoke(MyHud.Chat, steamUserId, message); } private void Multiplayer_ScriptedChatMessageReceived(string message, string author, string font) { var torchMsg = new TorchChatMessage(author, message, font); - var consumed = false; - MessageRecieved?.Invoke(torchMsg, ref consumed); - if (!consumed && HasHud) + if (!RaiseMessageRecieved(torchMsg) && HasHud) _hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font); } + protected bool RaiseMessageRecieved(TorchChatMessage msg) + { + var consumed = false; + MessageRecieved?.Invoke(msg, ref consumed); + return consumed; + } + private const string _hudChatMessageReceivedName = "Multiplayer_ChatMessageReceived"; private const string _hudChatScriptedMessageReceivedName = "multiplayer_ScriptedChatMessageReceived"; diff --git a/Torch/Managers/ChatManager/ChatManagerServer.cs b/Torch/Managers/ChatManager/ChatManagerServer.cs index 096e489..9fca462 100644 --- a/Torch/Managers/ChatManager/ChatManagerServer.cs +++ b/Torch/Managers/ChatManager/ChatManagerServer.cs @@ -41,6 +41,11 @@ namespace Torch.Managers.ChatManager /// public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0) { + if (targetSteamId == Sync.MyId) + { + RaiseMessageRecieved(new TorchChatMessage(authorId, message)); + return; + } if (MyMultiplayer.Static == null) { if ((targetSteamId == MyGameService.UserId || targetSteamId == 0) && HasHud) @@ -53,7 +58,6 @@ namespace Torch.Managers.ChatManager var msg = new ChatMsg() { Author = authorId, Text = message }; _dedicatedServerBaseSendChatMessage.Invoke(ref msg); _dedicatedServerBaseOnChatMessage.Invoke(dedicated, new object[] { msg }); - } } @@ -71,6 +75,11 @@ namespace Torch.Managers.ChatManager /// public void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0) { + if (targetSteamId == Sync.MyId) + { + RaiseMessageRecieved(new TorchChatMessage(author, message, font)); + return; + } if (MyMultiplayer.Static == null) { if ((targetSteamId == MyGameService.UserId || targetSteamId == 0) && HasHud) diff --git a/Torch/Patches/TorchAsyncSaving.cs b/Torch/Patches/TorchAsyncSaving.cs new file mode 100644 index 0000000..a26173f --- /dev/null +++ b/Torch/Patches/TorchAsyncSaving.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Sandbox; +using Sandbox.Engine.Platform; +using Sandbox.Game.Gui; +using Sandbox.Game.Screens.Helpers; +using Sandbox.Game.World; +using Sandbox.Graphics.GUI; +using Torch.API; +using Torch.API.Session; +using VRage; +using VRage.Audio; +using VRage.Utils; + +namespace Torch.Patches +{ + /// + /// A copy of except with decent async support. + /// + public class TorchAsyncSaving + { + /// + /// Saves the game asynchronously + /// + /// Torch instance + /// time in milliseconds before the save is treated as failed, or -1 to wait forever + /// New save name, or null for current name + /// Async result of save operation + public static Task Save(ITorchBase torch, int timeoutMs = -1, string newSaveName = null) + { + Task task = SaveInternal(torch, newSaveName); + return Task.Run(() => + { + // ReSharper disable once ConvertIfStatementToReturnStatement + if (timeoutMs >= 0 && !task.IsCompleted && !task.Wait(timeoutMs)) + return GameSaveResult.TimedOut; + return task.Result; + }); + } + + private static Task SaveInternal(ITorchBase torch, string newSaveName) + { + if (!MySandboxGame.IsGameReady) + return Task.FromResult(GameSaveResult.GameNotReady); + + var saveTaskSource = new TaskCompletionSource(); + torch.Invoke(() => + { + bool snapshotSuccess = MySession.Static.Save(out MySessionSnapshot tmpSnapshot, newSaveName); + if (!snapshotSuccess) + { + saveTaskSource.SetResult(GameSaveResult.FailedToTakeSnapshot); + return; + } + + if (!Game.IsDedicated) + TakeSaveScreenshot(); + tmpSnapshot.SaveParallel(() => + { + if (!Game.IsDedicated && MySession.Static != null) + ShowWorldSaveResult(tmpSnapshot.SavingSuccess); + saveTaskSource.SetResult(tmpSnapshot.SavingSuccess ? GameSaveResult.Success : GameSaveResult.FailedToSaveToDisk); + }); + }); + return saveTaskSource.Task; + } + + private static void ShowWorldSaveResult(bool success) + { + if (success) + { + var myHudNotification = new MyHudNotification(MyCommonTexts.WorldSaved); + myHudNotification.SetTextFormatArguments(MySession.Static.Name); + MyHud.Notifications.Add(myHudNotification); + } + else + { + MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, + MyMessageBoxButtonsType.OK, + new StringBuilder().AppendFormat(MyTexts.GetString(MyCommonTexts.WorldNotSaved), + MySession.Static.Name), MyTexts.Get(MyCommonTexts.MessageBoxCaptionError), null, null, null, + null, null, 0, MyGuiScreenMessageBox.ResultEnum.YES, true, null)); + } + } + + private static void TakeSaveScreenshot() + { + string thumbPath = MySession.Static.ThumbPath; + try + { + if (File.Exists(thumbPath)) + { + File.Delete(thumbPath); + } + MyGuiSandbox.TakeScreenshot(1200, 672, thumbPath, true, false); + } + catch (Exception ex) + { + MySandboxGame.Log.WriteLine("Could not take session thumb screenshot. Exception:"); + MySandboxGame.Log.WriteLine(ex); + } + } + } +} \ No newline at end of file diff --git a/Torch/SaveGameStatus.cs b/Torch/SaveGameStatus.cs deleted file mode 100644 index 824476c..0000000 --- a/Torch/SaveGameStatus.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Torch -{ - /// - /// Describes the possible outcomes when attempting to save the game progress. - /// - public enum SaveGameStatus : byte - { - /// - /// The game was saved. - /// - Success = 0, - - /// - /// A save operation is already in progress. - /// - SaveInProgress = 1, - - /// - /// The game is not in a save-able state. - /// - GameNotReady = 2, - - /// - /// The save operation timed out. - /// - TimedOut = 3 - }; -} diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index d999355..78c290b 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -196,6 +196,7 @@ + diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs index 83b6dc1..4a0c7ea 100644 --- a/Torch/TorchBase.cs +++ b/Torch/TorchBase.cs @@ -214,33 +214,6 @@ namespace Torch return Thread.CurrentThread.ManagedThreadId == MySandboxGame.Static.UpdateThread.ManagedThreadId; } - public Task SaveGameAsync(Action callback) - { - Log.Info("Saving game"); - - if (!MySandboxGame.IsGameReady) - { - callback?.Invoke(SaveGameStatus.GameNotReady); - } - else if (MyAsyncSaving.InProgress) - { - callback?.Invoke(SaveGameStatus.SaveInProgress); - } - else - { - var e = new AutoResetEvent(false); - MyAsyncSaving.Start(() => e.Set()); - - return Task.Run(() => - { - callback?.Invoke(e.WaitOne(5000) ? SaveGameStatus.Success : SaveGameStatus.TimedOut); - e.Dispose(); - }); - } - - return Task.CompletedTask; - } - #region Game Actions /// @@ -355,8 +328,16 @@ namespace Torch PatchManager.CommitInternal(); } + /// + /// Dispose callback for VRage plugin. Do not use. + /// + [Obsolete("Do not use; only there for VRage capability")] + public void Dispose() + { + } + /// - public virtual void Dispose() + public virtual void Destroy() { Managers.Detach(); _game.SignalDestroy(); @@ -375,12 +356,35 @@ namespace Torch protected virtual void TweakGameSettings() { } - + + private int _inProgressSaves = 0; /// - public virtual Task Save(long callerId) + public virtual Task Save(int timeoutMs = -1, bool exclusive = false) { - return SaveGameAsync(null); + if (exclusive) + { + if (MyAsyncSaving.InProgress || Interlocked.Increment(ref _inProgressSaves) != 1) + { + Log.Error("Failed to save game, game is already saving"); + return null; + } + } + return TorchAsyncSaving.Save(this, timeoutMs).ContinueWith((task, torchO) => + { + var torch = (TorchBase) torchO; + Interlocked.Decrement(ref torch._inProgressSaves); + if (task.IsFaulted) + { + Log.Error(task.Exception, "Failed to save game"); + return GameSaveResult.UnknownError; + } + if (task.Result != GameSaveResult.Success) + Log.Error($"Failed to save game: {task.Result}"); + else + Log.Info("Saved game"); + return task.Result; + }, this, TaskContinuationOptions.RunContinuationsAsynchronously); } ///