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)
This commit is contained in:
Westin Miller
2017-12-02 21:19:09 -08:00
parent 897f75c069
commit d07caea0f6
17 changed files with 348 additions and 166 deletions

View File

@@ -18,21 +18,25 @@ namespace Torch.API
/// <summary>
/// Fired when the session begins loading.
/// </summary>
[Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")]
event Action SessionLoading;
/// <summary>
/// Fired when the session finishes loading.
/// </summary>
[Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")]
event Action SessionLoaded;
/// <summary>
/// Fires when the session begins unloading.
/// </summary>
[Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")]
event Action SessionUnloading;
/// <summary>
/// Fired when the session finishes unloading.
/// </summary>
[Obsolete("Prefer using the TorchSessionManager.SessionStateChanged event")]
event Action SessionUnloaded;
/// <summary>
@@ -90,15 +94,17 @@ namespace Torch.API
void Stop();
/// <summary>
/// Restart the Torch instance.
/// Restart the Torch instance, blocking until the restart has been performed.
/// </summary>
void Restart();
/// <summary>
/// Initializes a save of the game.
/// </summary>
/// <param name="callerId">Id of the player who initiated the save.</param>
Task Save(long callerId);
/// <param name="timeoutMs">timeout before the save is treated as failed, or -1 for no timeout</param>
/// <param name="exclusive">Only start saving if we aren't already saving</param>
/// <returns>Future result of the save, or null if one is in progress and in exclusive mode</returns>
Task<GameSaveResult> Save(int timeoutMs = -1, bool exclusive = false);
/// <summary>
/// Initialize the Torch instance. Before this <see cref="Start"/> is invalid.
@@ -108,7 +114,7 @@ namespace Torch.API
/// <summary>
/// Disposes the Torch instance. After this <see cref="Start"/> is invalid.
/// </summary>
void Dispose();
void Destroy();
/// <summary>
/// The current state of the game this instance of torch is controlling.

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Session
{
/// <summary>
/// The result of a save operation
/// </summary>
public enum GameSaveResult
{
/// <summary>
/// Successfully saved
/// </summary>
Success = 0,
/// <summary>
/// The game wasn't ready to be saved
/// </summary>
GameNotReady = -1,
/// <summary>
/// Failed to take the snapshot of the current world state
/// </summary>
FailedToTakeSnapshot = -2,
/// <summary>
/// Failed to save the snapshot to disk
/// </summary>
FailedToSaveToDisk = -3,
/// <summary>
/// An unknown error occurred
/// </summary>
UnknownError = -4,
/// <summary>
/// The save operation timed out
/// </summary>
TimedOut = -5
}
}

View File

@@ -187,6 +187,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ServerState.cs" />
<Compile Include="ModAPI\TorchAPI.cs" />
<Compile Include="Session\GameSaveResult.cs" />
<Compile Include="Session\ITorchSession.cs" />
<Compile Include="Session\ITorchSessionManager.cs" />
<Compile Include="Session\TorchSessionState.cs" />

View File

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

View File

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

View File

@@ -178,16 +178,22 @@ namespace Torch.Server
/// </summary>
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);
}
/// <inheritdoc />
@@ -295,47 +301,5 @@ namespace Torch.Server
}
#endregion
/// <inheritdoc/>
public override Task Save(long callerId)
{
return SaveGameAsync(statusCode => SaveCompleted(statusCode, callerId));
}
/// <summary>
/// Callback for when save has finished.
/// </summary>
/// <param name="statusCode">Return code of the save operation</param>
/// <param name="callerId">Caller of the save operation</param>
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<IChatManagerServer>()?.SendMessageAsOther("Server", response,
statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId);
}
}
}
}

View File

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

View File

@@ -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.
/// </summary>
public ITorchPlugin Plugin { get; }
/// <summary>
/// The current Torch instance.
/// </summary>
public ITorchBase Torch { get; }
/// <summary>
/// The player who ran the command.
/// The player who ran the command, or null if the server sent it.
/// </summary>
public IMyPlayer Player { get; }
public IMyPlayer Player => Torch.CurrentSession.KeenSession.Players.TryGetPlayerBySteamId(_steamIdSender);
/// <summary>
/// Was this message sent by this program.
/// </summary>
public bool SentBySelf => _steamIdSender == Sync.MyId;
private ulong _steamIdSender;
/// <summary>
/// The command arguments split by spaces and quotes. Ex. "this is" a command -> {this is, a, command}
/// </summary>
public List<string> Args { get; }
/// <summary>
/// The non-split argument string.
/// </summary>
public string RawArgs { get; }
public string Response { get; private set; }
public CommandContext(ITorchBase torch, ITorchPlugin plugin, IMyPlayer player, string rawArgs = null, List<string> args = null)
public CommandContext(ITorchBase torch, ITorchPlugin plugin, ulong steamIdSender, string rawArgs = null,
List<string> args = null)
{
Torch = torch;
Plugin = plugin;
Player = player;
_steamIdSender = steamIdSender;
RawArgs = rawArgs;
Args = args ?? new List<string>();
}
public void Respond(string message, string sender = "Server", string font = MyFontEnum.Blue)
{
Response = message;
if (Player != null)
Torch.CurrentSession.Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther(sender, message, font, Player.SteamUserId);
Torch.CurrentSession.Managers.GetManager<IChatManagerServer>()
?.SendMessageAsOther(sender, message, font, _steamIdSender);
}
}
}

View File

@@ -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<Match>().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<Match>().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))

View File

@@ -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<PluginManager>()?.Plugins.Select(p => p.Value.Name) ?? Enumerable.Empty<string>();
var plugins = Context.Torch.Managers.GetManager<PluginManager>()?.Plugins.Select(p => p.Value.Name) ??
Enumerable.Empty<string>();
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<IChatManagerClient>().SendMessageAsSelf($"Restarting server in {i / 60} minute{Pluralize(i / 60)}.");
Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>()
.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<IChatManagerClient>().SendMessageAsSelf($"Restarting server in {i} second{Pluralize(i)}.");
Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>()
.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<GameSaveResult> 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);
}
}
}

View File

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

View File

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

View File

@@ -41,6 +41,11 @@ namespace Torch.Managers.ChatManager
/// <inheritdoc />
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
/// <inheritdoc />
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)

View File

@@ -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
{
/// <summary>
/// A copy of <see cref="MyAsyncSaving"/> except with decent async support.
/// </summary>
public class TorchAsyncSaving
{
/// <summary>
/// Saves the game asynchronously
/// </summary>
/// <param name="torch">Torch instance</param>
/// <param name="timeoutMs">time in milliseconds before the save is treated as failed, or -1 to wait forever</param>
/// <param name="newSaveName">New save name, or null for current name</param>
/// <returns>Async result of save operation</returns>
public static Task<GameSaveResult> Save(ITorchBase torch, int timeoutMs = -1, string newSaveName = null)
{
Task<GameSaveResult> 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<GameSaveResult> SaveInternal(ITorchBase torch, string newSaveName)
{
if (!MySandboxGame.IsGameReady)
return Task.FromResult(GameSaveResult.GameNotReady);
var saveTaskSource = new TaskCompletionSource<GameSaveResult>();
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);
}
}
}
}

View File

@@ -1,28 +0,0 @@
namespace Torch
{
/// <summary>
/// Describes the possible outcomes when attempting to save the game progress.
/// </summary>
public enum SaveGameStatus : byte
{
/// <summary>
/// The game was saved.
/// </summary>
Success = 0,
/// <summary>
/// A save operation is already in progress.
/// </summary>
SaveInProgress = 1,
/// <summary>
/// The game is not in a save-able state.
/// </summary>
GameNotReady = 2,
/// <summary>
/// The save operation timed out.
/// </summary>
TimedOut = 3
};
}

View File

@@ -196,6 +196,7 @@
<Compile Include="Patches\GameAnalyticsPatch.cs" />
<Compile Include="Patches\GameStatePatchShim.cs" />
<Compile Include="Patches\ObjectFactoryInitPatch.cs" />
<Compile Include="Patches\TorchAsyncSaving.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SaveGameStatus.cs" />
<Compile Include="Collections\KeyTree.cs" />

View File

@@ -214,33 +214,6 @@ namespace Torch
return Thread.CurrentThread.ManagedThreadId == MySandboxGame.Static.UpdateThread.ManagedThreadId;
}
public Task SaveGameAsync(Action<SaveGameStatus> 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
/// <summary>
@@ -355,8 +328,16 @@ namespace Torch
PatchManager.CommitInternal();
}
/// <summary>
/// Dispose callback for VRage plugin. Do not use.
/// </summary>
[Obsolete("Do not use; only there for VRage capability")]
public void Dispose()
{
}
/// <inheritdoc />
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;
/// <inheritdoc/>
public virtual Task Save(long callerId)
public virtual Task<GameSaveResult> 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);
}
/// <inheritdoc/>