diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-04-23 21:51:49 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-04-23 21:51:49 -0400 |
commit | 86ef70feece302f21041204b3baa31d757730acc (patch) | |
tree | 2607f445d592667f2894cc79765b980905b450f6 /src | |
parent | 5f595d8a464174bf8ec6940476cf6e05014e17b4 (diff) | |
download | SMAPI-86ef70feece302f21041204b3baa31d757730acc.tar.gz SMAPI-86ef70feece302f21041204b3baa31d757730acc.tar.bz2 SMAPI-86ef70feece302f21041204b3baa31d757730acc.zip |
revamp startup process (#265)
This revamps SMAPI's startup process to simplify mod development by ensuring that core components are ready by the time mods are loaded (which is also needed for the upcoming content API), and eliminate or reduce SEHExceptions some players experience.
Diffstat (limited to 'src')
-rw-r--r-- | src/StardewModdingAPI/Events/GameEvents.cs | 12 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/SGame.cs | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI/Program.cs | 356 |
3 files changed, 186 insertions, 184 deletions
diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 47c1275b..babf7f31 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using StardewModdingAPI.Framework; namespace StardewModdingAPI.Events @@ -16,7 +17,10 @@ namespace StardewModdingAPI.Events /********* ** Events *********/ - /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called during <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> + /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> + internal static event EventHandler InitializeInternal; + + /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] public static event EventHandler Initialize; @@ -66,12 +70,14 @@ namespace StardewModdingAPI.Events /// <param name="monitor">Encapsulates logging and monitoring.</param> internal static void InvokeInitialize(IMonitor monitor) { + // notify SMAPI + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); + + // notify mods if (GameEvents.Initialize == null) return; - string name = $"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}"; Delegate[] handlers = GameEvents.Initialize.GetInvocationList(); - GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.10", DeprecationLevel.Info); monitor.SafelyRaisePlainEvent(name, handlers); } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 99b9f991..39cc5cde 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -196,6 +196,8 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.FirstUpdate = true; SGame.Instance = this; + + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley } /**** diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 678fa90d..3bd91a7c 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -10,7 +9,6 @@ using System.Threading; using System.Management; using System.Windows.Forms; #endif -using Microsoft.Xna.Framework.Graphics; using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; @@ -38,26 +36,30 @@ namespace StardewModdingAPI /// <summary>The core logger for SMAPI.</summary> private readonly Monitor Monitor; - /// <summary>The SMAPI configuration settings.</summary> - private readonly SConfig Settings; - /// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary> private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); - /// <summary>Whether the game is currently running.</summary> - private bool IsGameRunning; - /// <summary>The underlying game instance.</summary> private SGame GameInstance; + /// <summary>The SMAPI configuration settings.</summary> + /// <remarks>This is initialised after the game starts.</remarks> + private SConfig Settings; + /// <summary>Tracks the installed mods.</summary> - private readonly ModRegistry ModRegistry; + /// <remarks>This is initialised after the game starts.</remarks> + private ModRegistry ModRegistry; /// <summary>Manages deprecation warnings.</summary> - private readonly DeprecationManager DeprecationManager; + /// <remarks>This is initialised after the game starts.</remarks> + private DeprecationManager DeprecationManager; /// <summary>Manages console commands.</summary> - private readonly CommandManager CommandManager = new CommandManager(); + /// <remarks>This is initialised after the game starts.</remarks> + private CommandManager CommandManager; + + /// <summary>Whether the game is currently running.</summary> + private bool IsGameRunning; /********* @@ -65,7 +67,7 @@ namespace StardewModdingAPI *********/ /// <summary>The main entry point which hooks into and launches the game.</summary> /// <param name="args">The command-line arguments.</param> - private static void Main(string[] args) + public static void Main(string[] args) { // get flags from arguments bool writeToConsole = !args.Contains("--no-terminal"); @@ -92,65 +94,28 @@ namespace StardewModdingAPI /// <summary>Construct an instance.</summary> /// <param name="writeToConsole">Whether to output log messages to the console.</param> /// <param name="logPath">The full file path to which to write log messages.</param> - internal Program(bool writeToConsole, string logPath) + public Program(bool writeToConsole, string logPath) { - // load settings - this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)); - - // initialise this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; - this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); - this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); } /// <summary>Launch SMAPI.</summary> - internal void LaunchInteractively() + public void LaunchInteractively() { - // initialise logging - Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}"; - - // inject compatibility shims -#pragma warning disable 618 - Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); - Config.Shim(this.DeprecationManager); - InternalExtensions.Shim(this.ModRegistry); - Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); - Mod.Shim(this.DeprecationManager); - ContentEvents.Shim(this.ModRegistry, this.Monitor); - GameEvents.Shim(this.DeprecationManager); - PlayerEvents.Shim(this.DeprecationManager); - TimeEvents.Shim(this.DeprecationManager); -#pragma warning restore 618 - - // redirect direct console output - { - Monitor monitor = this.GetSecondaryMonitor("Console.Out"); - monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion - if (monitor.WriteToConsole) - this.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); - } - - // add warning headers - if (this.Settings.DeveloperMode) + // initialise SMAPI + try { - this.Monitor.ShowTraceInConsole = true; - this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Warn); - } - if (!this.Settings.CheckForUpdates) - this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); - if (!this.Monitor.WriteToConsole) - this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + // init logging + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + this.Monitor.Log($"Mods go here: {Constants.ModPath}"); + this.Monitor.Log("Preparing SMAPI..."); - // print file paths - this.Monitor.Log($"Mods go here: {Constants.ModPath}"); + // validate paths + this.VerifyPath(Constants.ModPath); + this.VerifyPath(Constants.LogDir); - // hook into & launch the game - try - { - // verify version + // validate game version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); @@ -164,29 +129,63 @@ namespace StardewModdingAPI return; } - // initialise folders - this.Monitor.Log("Loading SMAPI..."); - this.VerifyPath(Constants.ModPath); - this.VerifyPath(Constants.LogDir); + // add error handlers +#if SMAPI_FOR_WINDOWS + Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); +#endif + AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + + // override game & hook events + this.GameInstance = new SGame(this.Monitor); +#if SDV_1_2 + StardewValley.Program.gamePtr = this.GameInstance; +#else + { + Type type = typeof(Game1).Assembly.GetType("StardewValley.Program", true); + type.GetField("gamePtr").SetValue(null, this.GameInstance); + } +#endif - // check for update when game loads - if (this.Settings.CheckForUpdates) - GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); + // hook into game events + this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; + this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); + GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); + GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); - // launch game - this.StartGame(); + // set window titles + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}"; } catch (Exception ex) { - this.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + + // start game + this.Monitor.Log("Starting game..."); + try + { + this.IsGameRunning = true; + this.GameInstance.Run(); + } + catch (Exception ex) + { + this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + } + finally + { + this.IsGameRunning = false; } - this.PressAnyKeyToExit(); } /// <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> - internal void ExitGameImmediately(string module, string reason) + public void ExitGameImmediately(string module, string reason) { this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}"); this.CancellationTokenSource.Cancel(); @@ -209,9 +208,106 @@ namespace StardewModdingAPI /********* ** Private methods *********/ + /// <summary>Initialise SMAPI and mods after the game starts.</summary> + private void InitialiseAfterGameStart() + { + // load settings + this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)); + + // load core components + this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); + this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + this.CommandManager = new CommandManager(); + + // inject compatibility shims +#pragma warning disable 618 + Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); + Config.Shim(this.DeprecationManager); + InternalExtensions.Shim(this.ModRegistry); + Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); + Mod.Shim(this.DeprecationManager); + ContentEvents.Shim(this.ModRegistry, this.Monitor); + GameEvents.Shim(this.DeprecationManager); + PlayerEvents.Shim(this.DeprecationManager); + TimeEvents.Shim(this.DeprecationManager); +#pragma warning restore 618 + + // redirect direct console output + { + Monitor monitor = this.GetSecondaryMonitor("Console.Out"); + monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion + if (monitor.WriteToConsole) + this.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); + } + + // add warning headers + if (this.Settings.DeveloperMode) + { + this.Monitor.ShowTraceInConsole = true; + this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Warn); + } + if (!this.Settings.CheckForUpdates) + this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + if (!this.Monitor.WriteToConsole) + this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + + // load mods + int modsLoaded = this.LoadMods(); + if (this.CancellationTokenSource.IsCancellationRequested) + { + this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); + return; + } + + // update window titles + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods"; + + // start SMAPI console + new Thread(this.RunConsoleLoop).Start(); + } + + /// <summary>Run a loop handling console input.</summary> + [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] + private void RunConsoleLoop() + { + // prepare help command + this.Monitor.Log("Starting console..."); + this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info); + this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help <cmd>' returns command description", this.HandleHelpCommand); + + // start handling command line input + Thread inputThread = new Thread(() => + { + while (true) + { + string input = Console.ReadLine(); + try + { + if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input)) + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + } + catch (Exception ex) + { + this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + }); + inputThread.Start(); + + // keep console thread alive while the game is running + while (this.IsGameRunning) + Thread.Sleep(1000 / 10); + if (inputThread.ThreadState == ThreadState.Running) + inputThread.Abort(); + } + /// <summary>Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available.</summary> private void CheckForUpdateAsync() { + if (!this.Settings.CheckForUpdates) + return; + new Thread(() => { try @@ -228,91 +324,6 @@ namespace StardewModdingAPI }).Start(); } - /// <summary>Hook into Stardew Valley and launch the game.</summary> - private void StartGame() - { - try - { - this.Monitor.Log("Loading game..."); - - // add error handlers -#if SMAPI_FOR_WINDOWS - Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); -#endif - AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - - // override Game1 instance - this.GameInstance = new SGame(this.Monitor); - this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; - this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} with SMAPI {Constants.ApiVersion}"; -#if SDV_1_2 - StardewValley.Program.gamePtr = this.GameInstance; -#else - { - Type type = typeof(Game1).Assembly.GetType("StardewValley.Program", true); - type.GetField("gamePtr").SetValue(null, this.GameInstance); - } -#endif - - // configure - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - - // load mods - this.LoadMods(); - if (this.CancellationTokenSource.IsCancellationRequested) - { - this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); - return; - } - - // initialise console after game launches - new Thread(() => - { - // wait for the game to load up - while (!this.IsGameRunning) - Thread.Sleep(1000); - - // register help command - this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help <cmd>' returns command description", this.HandleHelpCommand); - - // listen for command line input - this.Monitor.Log("Starting console..."); - this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info); - Thread consoleInputThread = new Thread(this.ConsoleInputLoop); - consoleInputThread.Start(); - while (this.IsGameRunning) - Thread.Sleep(1000 / 10); // Check if the game is still running 10 times a second - - // abort the console thread, we're closing - if (consoleInputThread.ThreadState == ThreadState.Running) - consoleInputThread.Abort(); - }).Start(); - - // start game loop - this.Monitor.Log("Starting game..."); - if (this.CancellationTokenSource.IsCancellationRequested) - { - this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); - return; - } - try - { - this.IsGameRunning = true; - this.GameInstance.Run(); - } - finally - { - this.IsGameRunning = false; - } - } - catch (Exception ex) - { - this.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - /// <summary>Create a directory path if it doesn't exist.</summary> /// <param name="path">The directory path.</param> private void VerifyPath(string path) @@ -329,7 +340,8 @@ namespace StardewModdingAPI } /// <summary>Load and hook up all mods in the mod directory.</summary> - private void LoadMods() + /// <returns>Returns the number of mods loaded.</returns> + private int LoadMods() { this.Monitor.Log("Loading mods..."); @@ -354,7 +366,7 @@ namespace StardewModdingAPI if (this.CancellationTokenSource.IsCancellationRequested) { this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); - return; + return modsLoaded; } // get manifest path @@ -553,12 +565,12 @@ namespace StardewModdingAPI } // initialise mods - foreach (Mod mod in this.ModRegistry.GetMods()) + foreach (IMod mod in this.ModRegistry.GetMods()) { try { // call entry methods - mod.Entry(); // deprecated since 1.0 + (mod as Mod)?.Entry(); // deprecated since 1.0 mod.Entry(mod.Helper); // raise deprecation warning for old Entry() methods @@ -575,26 +587,8 @@ namespace StardewModdingAPI this.Monitor.Log($"Loaded {modsLoaded} mods."); foreach (Action warning in deprecationWarnings) warning(); - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods"; - } - /// <summary>Run a loop handling console input.</summary> - [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] - private void ConsoleInputLoop() - { - while (true) - { - string input = Console.ReadLine(); - try - { - if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input)) - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); - } - catch (Exception ex) - { - this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } + return modsLoaded; } /// <summary>The method called when the user submits the help command in the console.</summary> |