diff options
-rw-r--r-- | release-notes.md | 1 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/InternalExtensions.cs | 8 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Monitor.cs | 31 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/SGame.cs | 7 | ||||
-rw-r--r-- | src/StardewModdingAPI/IMonitor.cs | 7 | ||||
-rw-r--r-- | src/StardewModdingAPI/Program.cs | 50 |
6 files changed, 64 insertions, 40 deletions
diff --git a/release-notes.md b/release-notes.md index 311128d0..acb584cd 100644 --- a/release-notes.md +++ b/release-notes.md @@ -19,6 +19,7 @@ For players: For mod developers: * `Console.Out` messages are now written to the log file. +* `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly. ## 1.10 See [log](https://github.com/Pathoschild/SMAPI/compare/1.9...1.10). diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index a2d589ff..2f6f7490 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -41,6 +41,14 @@ namespace StardewModdingAPI.Framework foreach (EventHandler handler in handlers.Cast<EventHandler>()) { + // handle SMAPI exiting + if (monitor.IsExiting) + { + monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn); + return; + } + + // raise event try { handler.Invoke(sender, args ?? EventArgs.Empty); diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index dac81f14..925efc33 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using StardewModdingAPI.Framework.Logging; namespace StardewModdingAPI.Framework @@ -34,13 +35,16 @@ namespace StardewModdingAPI.Framework [LogLevel.Alert] = ConsoleColor.Magenta }; - /// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary> - private readonly RequestExitDelegate RequestExit; + /// <summary>Propagates notification that SMAPI should exit.</summary> + private readonly CancellationTokenSource ExitTokenSource; /********* ** Accessors *********/ + /// <summary>Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks.</summary> + public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; + /// <summary>Whether to show trace messages in the console.</summary> internal bool ShowTraceInConsole { get; set; } @@ -58,8 +62,8 @@ namespace StardewModdingAPI.Framework /// <param name="source">The name of the module which logs messages using this instance.</param> /// <param name="consoleManager">Manages access to the console output.</param> /// <param name="logFile">The log file to which to write messages.</param> - /// <param name="requestExitDelegate">A delegate which requests that SMAPI immediately exit the game.</param> - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, RequestExitDelegate requestExitDelegate) + /// <param name="exitTokenSource">Propagates notification that SMAPI should exit.</param> + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -69,7 +73,7 @@ namespace StardewModdingAPI.Framework this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleManager = consoleManager; - this.RequestExit = requestExitDelegate; + this.ExitTokenSource = exitTokenSource; } /// <summary>Log a message for the player or developer.</summary> @@ -84,14 +88,8 @@ namespace StardewModdingAPI.Framework /// <param name="reason">The reason for the shutdown.</param> public void ExitGameImmediately(string reason) { - this.RequestExit(this.Source, reason); - } - - /// <summary>Log a fatal error message.</summary> - /// <param name="message">The message to log.</param> - internal void LogFatal(string message) - { - this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); + this.LogFatal($"{this.Source} requested an immediate game shutdown: {reason}"); + this.ExitTokenSource.Cancel(); } /// <summary>Log a message for the player or developer, using the specified console color.</summary> @@ -109,6 +107,13 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// <summary>Log a fatal error message.</summary> + /// <param name="message">The message to log.</param> + private void LogFatal(string message) + { + this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); + } + /// <summary>Write a message line to the log.</summary> /// <param name="source">The name of the mod logging the message.</param> /// <param name="message">The message to log.</param> diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 61493e87..e7c07889 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -200,6 +200,13 @@ namespace StardewModdingAPI.Framework /// <param name="gameTime">A snapshot of the game timing state.</param> protected override void Update(GameTime gameTime) { + // SMAPI exiting, stop processing game updates + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); + return; + } + // While a background new-day task is in progress, the game skips its own update logic // and defers to the XNA Update method. Running mod code in parallel to the background // update is risky, because data changes can conflict (e.g. collection changed during diff --git a/src/StardewModdingAPI/IMonitor.cs b/src/StardewModdingAPI/IMonitor.cs index 571c7403..62c479bc 100644 --- a/src/StardewModdingAPI/IMonitor.cs +++ b/src/StardewModdingAPI/IMonitor.cs @@ -4,6 +4,13 @@ public interface IMonitor { /********* + ** Accessors + *********/ + /// <summary>Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks.</summary> + bool IsExiting { get; } + + + /********* ** Methods *********/ /// <summary>Log a message for the player or developer.</summary> diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 6e57fd65..57ec1a17 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI /// <summary>Manages console output interception.</summary> private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); - /// <summary>The core logger for SMAPI.</summary> + /// <summary>The core logger and monitor for SMAPI.</summary> private readonly Monitor Monitor; /// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary> @@ -99,7 +99,7 @@ namespace StardewModdingAPI public Program(bool writeToConsole, string logPath) { this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; } /// <summary>Launch SMAPI.</summary> @@ -142,6 +142,17 @@ namespace StardewModdingAPI this.GameInstance = new SGame(this.Monitor); StardewValley.Program.gamePtr = this.GameInstance; + // add exit handler + new Thread(() => + { + this.CancellationTokenSource.Token.WaitHandle.WaitOne(); + if (this.IsGameRunning) + { + this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit(); + this.GameInstance.Exit(); + } + }).Start(); + // hook into game events #if SMAPI_FOR_WINDOWS ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); @@ -180,20 +191,6 @@ namespace StardewModdingAPI } } - /// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary> - /// <param name="module">The module which requested an immediate exit.</param> - /// <param name="reason">The reason provided for the shutdown.</param> - public void ExitGameImmediately(string module, string reason) - { - this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}"); - this.CancellationTokenSource.Cancel(); - if (this.IsGameRunning) - { - this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit(); - this.GameInstance.Exit(); - } - } - /// <summary>Get a monitor for legacy code which doesn't have one passed in.</summary> [Obsolete("This method should only be used when needed for backwards compatibility.")] internal IMonitor GetLegacyMonitorForMod() @@ -264,9 +261,9 @@ namespace StardewModdingAPI // load mods int modsLoaded = this.LoadMods(); - if (this.CancellationTokenSource.IsCancellationRequested) + if (this.Monitor.IsExiting) { - this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); + this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); return; } @@ -307,7 +304,7 @@ namespace StardewModdingAPI inputThread.Start(); // keep console thread alive while the game is running - while (this.IsGameRunning) + while (this.IsGameRunning && !this.Monitor.IsExiting) Thread.Sleep(1000 / 10); if (inputThread.ThreadState == ThreadState.Running) inputThread.Abort(); @@ -368,18 +365,17 @@ namespace StardewModdingAPI List<Action> deprecationWarnings = new List<Action>(); // queue up deprecation warnings to show after mod list foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath)) { + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn); + return modsLoaded; + } + // passthrough empty directories DirectoryInfo directory = new DirectoryInfo(directoryPath); while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) directory = directory.GetDirectories().First(); - // check for cancellation - if (this.CancellationTokenSource.IsCancellationRequested) - { - this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); - return modsLoaded; - } - // get manifest path string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) @@ -625,7 +621,7 @@ namespace StardewModdingAPI /// <param name="name">The name of the module which will log messages with this instance.</param> private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode }; + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode }; } /// <summary>Get a human-readable name for the current platform.</summary> |