diff options
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> |