From 46b7d7a4001e209d7201ed5cad38cad2f1ad2a7f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Feb 2017 01:15:56 -0500 Subject: redirect the game's debug messages into trace logs (#233) The game writes debug messages directly to the console, which shows up for SMAPI users. This commit redirects direct console messages to a monitor. --- src/StardewModdingAPI/Framework/Monitor.cs | 56 +++++++++++++----------------- 1 file changed, 24 insertions(+), 32 deletions(-) (limited to 'src/StardewModdingAPI/Framework/Monitor.cs') diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 39b567d8..33c1bbf4 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 StardewModdingAPI.Framework.Logging; namespace StardewModdingAPI.Framework { @@ -13,6 +14,9 @@ namespace StardewModdingAPI.Framework /// The name of the module which logs messages using this instance. private readonly string Source; + /// Manages access to the console output. + private readonly ConsoleInterceptionManager ConsoleManager; + /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -34,9 +38,6 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// Whether the current console supports color codes. - internal static readonly bool ConsoleSupportsColor = Monitor.GetConsoleSupportsColor(); - /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } @@ -49,8 +50,9 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. + /// Manages access to the console output. /// The log file to which to write messages. - public Monitor(string source, LogFileManager logFile) + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -61,6 +63,7 @@ namespace StardewModdingAPI.Framework // initialise this.Source = source; this.LogFile = logFile; + this.ConsoleManager = consoleManager; } /// Log a message for the player or developer. @@ -68,7 +71,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { - this.LogImpl(this.Source, message, Monitor.Colors[level], level); + this.LogImpl(this.Source, message, level, Monitor.Colors[level]); } /// 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. @@ -83,8 +86,7 @@ namespace StardewModdingAPI.Framework /// The message to log. internal void LogFatal(string message) { - Console.BackgroundColor = ConsoleColor.Red; - this.LogImpl(this.Source, message, ConsoleColor.White, LogLevel.Error); + this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); } /// Log a message for the player or developer, using the specified console color. @@ -95,7 +97,7 @@ namespace StardewModdingAPI.Framework [Obsolete("This method is provided for backwards compatibility and otherwise should not be used. Use " + nameof(Monitor) + "." + nameof(Monitor.Log) + " instead.")] internal void LegacyLog(string source, string message, ConsoleColor color, LogLevel level = LogLevel.Debug) { - this.LogImpl(source, message, color, level); + this.LogImpl(source, message, level, color); } @@ -105,9 +107,10 @@ namespace StardewModdingAPI.Framework /// Write a message line to the log. /// The name of the mod logging the message. /// The message to log. - /// The console color. /// The log level. - private void LogImpl(string source, string message, ConsoleColor color, LogLevel level) + /// The console foreground color. + /// The console background color (or null to leave it as-is). + private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) { // generate message string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); @@ -116,30 +119,19 @@ namespace StardewModdingAPI.Framework // log if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) { - if (Monitor.ConsoleSupportsColor) + this.ConsoleManager.ExclusiveWriteWithoutInterception(() => { - Console.ForegroundColor = color; - Console.WriteLine(message); - Console.ResetColor(); - } - else - Console.WriteLine(message); + if (this.ConsoleManager.SupportsColor) + { + Console.ForegroundColor = color; + Console.WriteLine(message); + Console.ResetColor(); + } + else + Console.WriteLine(message); + }); } this.LogFile.WriteLine(message); } - - /// Test whether the current console supports color formatting. - private static bool GetConsoleSupportsColor() - { - try - { - Console.ForegroundColor = Console.ForegroundColor; - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } } -} \ No newline at end of file +} -- cgit From 693f16f99ec3492e3bfcd0071af1d9d5e519dcfa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Feb 2017 02:08:21 -0500 Subject: don't write direct console output to log file (#233) Per discussion with mod developers. --- src/StardewModdingAPI/Framework/Monitor.cs | 10 ++++++++-- src/StardewModdingAPI/Program.cs | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Framework/Monitor.cs') diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 33c1bbf4..3a9276a0 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -44,6 +44,9 @@ namespace StardewModdingAPI.Framework /// Whether to write anything to the console. This should be disabled if no console is available. internal bool WriteToConsole { get; set; } = true; + /// Whether to write anything to the log file. This should almost always be enabled. + internal bool WriteToFile { get; set; } = true; + /********* ** Public methods @@ -116,7 +119,7 @@ namespace StardewModdingAPI.Framework string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); message = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; - // log + // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) { this.ConsoleManager.ExclusiveWriteWithoutInterception(() => @@ -131,7 +134,10 @@ namespace StardewModdingAPI.Framework Console.WriteLine(message); }); } - this.LogFile.WriteLine(message); + + // write to log file + if (this.WriteToFile) + this.LogFile.WriteLine(message); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 91a39042..f4518e21 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -112,8 +112,10 @@ namespace StardewModdingAPI // redirect direct console output { - IMonitor monitor = Program.GetSecondaryMonitor("Console.Out"); - Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); + Monitor monitor = Program.GetSecondaryMonitor("Console.Out"); + monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion + if (monitor.WriteToConsole) + Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); } // add warning headers -- cgit From f140e844ed1e52d1f5955e07a397f44ef9ab3233 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 14 Feb 2017 22:06:06 -0500 Subject: streamline startup a bit --- src/StardewModdingAPI/Framework/Monitor.cs | 2 +- src/StardewModdingAPI/Program.cs | 56 +++++++++++++----------------- 2 files changed, 26 insertions(+), 32 deletions(-) (limited to 'src/StardewModdingAPI/Framework/Monitor.cs') diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 3a9276a0..c1735917 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework public void ExitGameImmediately(string reason) { Program.ExitGameImmediately(this.Source, reason); - Program.gamePtr.Exit(); + Program.GameInstance.Exit(); } /// Log a fatal error message. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 58b86e8f..c3f8f8d8 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -59,21 +59,12 @@ namespace StardewModdingAPI /// Whether the game is currently running. private static bool ready; - /// The underlying game assembly. - private static Assembly StardewAssembly; - - /// The underlying type. - private static Type StardewProgramType; - - /// The field containing game's main instance. - private static FieldInfo StardewGameInfo; - /********* ** Accessors *********/ /// The underlying game instance. - internal static SGame gamePtr; + internal static SGame GameInstance; /// The number of mods currently loaded by SMAPI. internal static int ModsLoaded; @@ -185,8 +176,8 @@ namespace StardewModdingAPI Program.CancellationTokenSource.Cancel(); if (Program.ready) { - Program.gamePtr.Exiting += (sender, e) => Program.PressAnyKeyToExit(); - Program.gamePtr.Exit(); + Program.GameInstance.Exiting += (sender, e) => Program.PressAnyKeyToExit(); + Program.GameInstance.Exit(); } } @@ -226,29 +217,31 @@ namespace StardewModdingAPI { try { - // load the game assembly - Program.Monitor.Log("Loading game..."); - Program.StardewAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); - Program.StardewProgramType = Program.StardewAssembly.GetType("StardewValley.Program", true); - Program.StardewGameInfo = Program.StardewProgramType.GetField("gamePtr"); - Game1.version += $" | SMAPI {Constants.ApiVersion}"; - - // add error interceptors + // add error handlers #if SMAPI_FOR_WINDOWS Application.ThreadException += (sender, e) => Program.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => Program.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - // initialise game instance - Program.gamePtr = new SGame(Program.Monitor) { IsMouseVisible = false }; - Program.gamePtr.Exiting += (sender, e) => Program.ready = false; - Program.gamePtr.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e); - Program.gamePtr.Window.Title = $"Stardew Valley - Version {Game1.version}"; - Program.StardewGameInfo.SetValue(Program.StardewProgramType, Program.gamePtr); - - // patch graphics - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + // initialise game + { + // load assembly + Program.Monitor.Log("Loading game..."); + Assembly gameAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); + Type gameProgramType = gameAssembly.GetType("StardewValley.Program", true); + + // set Game1 instance + Program.GameInstance = new SGame(Program.Monitor); + Program.GameInstance.Exiting += (sender, e) => Program.ready = false; + Program.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e); + Program.GameInstance.Window.Title = $"Stardew Valley - Version {Game1.version}"; + gameProgramType.GetField("gamePtr").SetValue(gameProgramType, Program.GameInstance); + + // configure + Game1.version += $" | SMAPI {Constants.ApiVersion}"; + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + } // load mods Program.LoadMods(); @@ -262,7 +255,8 @@ namespace StardewModdingAPI new Thread(() => { // wait for the game to load up - while (!Program.ready) Thread.Sleep(1000); + while (!Program.ready) + Thread.Sleep(1000); // register help command Program.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", Program.HandleHelpCommand); @@ -290,7 +284,7 @@ namespace StardewModdingAPI try { Program.ready = true; - Program.gamePtr.Run(); + Program.GameInstance.Run(); } finally { -- cgit From 176eddbf7b70934c2665aa3a0ac8b46bef04012a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 Feb 2017 00:54:41 -0500 Subject: make SMAPI core non-static, eliminate direct access between core components --- src/StardewModdingAPI/Command.cs | 39 ++- src/StardewModdingAPI/Config.cs | 18 +- src/StardewModdingAPI/Constants.cs | 3 - src/StardewModdingAPI/Events/PlayerEvents.cs | 18 +- src/StardewModdingAPI/Events/TimeEvents.cs | 16 +- .../Framework/InternalExtensions.cs | 16 +- src/StardewModdingAPI/Framework/Monitor.cs | 9 +- .../Framework/RequestExitDelegate.cs | 7 + src/StardewModdingAPI/Log.cs | 16 +- src/StardewModdingAPI/Mod.cs | 24 +- src/StardewModdingAPI/Program.cs | 287 +++++++++++---------- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 12 files changed, 294 insertions(+), 160 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/RequestExitDelegate.cs (limited to 'src/StardewModdingAPI/Framework/Monitor.cs') diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs index 1cbb01ff..6b056ce7 100644 --- a/src/StardewModdingAPI/Command.cs +++ b/src/StardewModdingAPI/Command.cs @@ -12,12 +12,22 @@ namespace StardewModdingAPI /********* ** Properties *********/ - /**** - ** SMAPI - ****/ /// The commands registered with SMAPI. private static readonly IDictionary LegacyCommands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + /// Manages console commands. + private static CommandManager CommandManager; + + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /// Tracks the installed mods. + private static ModRegistry ModRegistry; + + + /********* + ** Accessors + *********/ /// The event raised when this command is submitted through the console. public event EventHandler CommandFired; @@ -43,6 +53,17 @@ namespace StardewModdingAPI /**** ** Command ****/ + /// Injects types required for backwards compatibility. + /// Manages console commands. + /// Manages deprecation warnings. + /// Tracks the installed mods. + internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry) + { + Command.CommandManager = commandManager; + Command.DeprecationManager = deprecationManager; + Command.ModRegistry = modRegistry; + } + /// Construct an instance. /// The name of the command. /// A human-readable description of what the command does. @@ -73,8 +94,8 @@ namespace StardewModdingAPI /// Encapsulates monitoring and logging. public static void CallCommand(string input, IMonitor monitor) { - Program.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Notice); - Program.CommandManager.Trigger(input); + Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Notice); + Command.CommandManager.Trigger(input); } /// Register a command with SMAPI. @@ -86,18 +107,18 @@ namespace StardewModdingAPI name = name?.Trim().ToLower(); // raise deprecation warning - Program.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Notice); // validate if (Command.LegacyCommands.ContainsKey(name)) throw new InvalidOperationException($"The '{name}' command is already registered!"); // add command - string modName = Program.ModRegistry.GetModFromStack() ?? ""; + string modName = Command.ModRegistry.GetModFromStack() ?? ""; string documentation = args?.Length > 0 ? $"{description} - {string.Join(", ", args)}" : description; - Program.CommandManager.Add(modName, name, documentation, Command.Fire); + Command.CommandManager.Add(modName, name, documentation, Command.Fire); // add legacy command Command command = new Command(name, description, args); @@ -109,7 +130,7 @@ namespace StardewModdingAPI /// The command name to find. public static Command FindCommand(string name) { - Program.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Notice); if (name == null) return null; diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs index 037c0fdf..f253930d 100644 --- a/src/StardewModdingAPI/Config.cs +++ b/src/StardewModdingAPI/Config.cs @@ -11,6 +11,13 @@ namespace StardewModdingAPI [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] public abstract class Config { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Accessors *********/ @@ -26,6 +33,13 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + Config.DeprecationManager = deprecationManager; + } + /// Construct an instance of the config class. /// The config class type. [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] @@ -111,8 +125,8 @@ namespace StardewModdingAPI /// Construct an instance. protected Config() { - Program.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Notice); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings + Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Notice); + Config.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings } } diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index db0c2622..262ba61d 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -59,9 +59,6 @@ namespace StardewModdingAPI /// The GitHub repository to check for updates. internal const string GitHubRepository = "Pathoschild/SMAPI"; - /// The title of the SMAPI console window. - internal static string ConsoleTitle => $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {Program.ModsLoaded}"; - /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index 99bdac16..996077ab 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -10,6 +10,13 @@ namespace StardewModdingAPI.Events /// Events raised when the player data changes. public static class PlayerEvents { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Events *********/ @@ -31,6 +38,13 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + PlayerEvents.DeprecationManager = deprecationManager; + } + /// Raise a event. /// Encapsulates monitoring and logging. /// Whether the save has been loaded. This is always true. @@ -42,7 +56,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}"; Delegate[] handlers = PlayerEvents.LoadedGame.GetInvocationList(); - Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); monitor.SafelyRaiseGenericEvent(name, handlers, null, loaded); } @@ -58,7 +72,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}"; Delegate[] handlers = PlayerEvents.FarmerChanged.GetInvocationList(); - Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); } diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index a140a223..0f9257c1 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -6,6 +6,13 @@ namespace StardewModdingAPI.Events /// Events raised when the in-game date or time changes. public static class TimeEvents { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Events *********/ @@ -32,6 +39,13 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + TimeEvents.DeprecationManager = deprecationManager; + } + /// Raise an event. /// Encapsulates monitoring and logging. internal static void InvokeAfterDayStarted(IMonitor monitor) @@ -88,7 +102,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}"; Delegate[] handlers = TimeEvents.OnNewDay.GetInvocationList(); - Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + TimeEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); } } diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index c4bd2d35..4ca79518 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -8,9 +8,23 @@ namespace StardewModdingAPI.Framework /// Provides extension methods for SMAPI's internal use. internal static class InternalExtensions { + /********* + ** Properties + *********/ + /// Tracks the installed mods. + private static ModRegistry ModRegistry; + + /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Tracks the installed mods. + internal static void Shim(ModRegistry modRegistry) + { + InternalExtensions.ModRegistry = modRegistry; + } + /**** ** IMonitor ****/ @@ -103,7 +117,7 @@ namespace StardewModdingAPI.Framework foreach (Delegate handler in handlers) { - string modName = Program.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here + string modName = InternalExtensions.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here deprecationManager.Warn(modName, nounPhrase, version, severity); } } diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index c1735917..64075f2f 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework [LogLevel.Alert] = ConsoleColor.Magenta }; + /// 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. + private RequestExitDelegate RequestExit; + /********* ** Accessors @@ -55,7 +58,8 @@ namespace StardewModdingAPI.Framework /// The name of the module which logs messages using this instance. /// Manages access to the console output. /// The log file to which to write messages. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile) + /// A delegate which requests that SMAPI immediately exit the game. + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, RequestExitDelegate requestExitDelegate) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -81,8 +85,7 @@ namespace StardewModdingAPI.Framework /// The reason for the shutdown. public void ExitGameImmediately(string reason) { - Program.ExitGameImmediately(this.Source, reason); - Program.GameInstance.Exit(); + this.RequestExit(this.Source, reason); } /// Log a fatal error message. diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs new file mode 100644 index 00000000..12d0ea0c --- /dev/null +++ b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Framework +{ + /// 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. + /// The module which requested an immediate exit. + /// The reason provided for the shutdown. + internal delegate void RequestExitDelegate(string module, string reason); +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs index 5cb794f9..da98baba 100644 --- a/src/StardewModdingAPI/Log.cs +++ b/src/StardewModdingAPI/Log.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Monitor))] public static class Log { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Accessors *********/ @@ -22,6 +29,13 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + Log.DeprecationManager = deprecationManager; + } + /**** ** Exceptions ****/ @@ -292,7 +306,7 @@ namespace StardewModdingAPI /// Raise a deprecation warning. private static void WarnDeprecated() { - Program.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Notice); + Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Notice); } /// Get the name of the mod logging a message from the stack. diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index c8456a29..d3fe882f 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -10,6 +10,9 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + /// The backing field for . private string _pathOnDisk; @@ -32,7 +35,7 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Notice); return this._pathOnDisk; } internal set { this._pathOnDisk = value; } @@ -44,8 +47,8 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Notice); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings return Path.Combine(this.PathOnDisk, "config.json"); } } @@ -60,8 +63,8 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.Info); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.Info); + Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings return Constants.CurrentSavePathExists ? Path.Combine(this.PerSaveConfigFolder, Constants.SaveFolderName + ".json") : ""; } } @@ -70,6 +73,13 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + Mod.DeprecationManager = deprecationManager; + } + /// The mod entry point, called after the mod is first loaded. [Obsolete("This overload is obsolete since SMAPI 1.0.")] public virtual void Entry(params object[] objects) { } @@ -86,8 +96,8 @@ namespace StardewModdingAPI [Obsolete] private string GetPerSaveConfigFolder() { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings if (!((Manifest)this.ModManifest).PerSaveConfigs) { diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index c3f8f8d8..0857d41b 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -28,7 +28,7 @@ namespace StardewModdingAPI ** Properties *********/ /// The target game platform. - private static readonly Platform TargetPlatform = + private readonly Platform TargetPlatform = #if SMAPI_FOR_WINDOWS Platform.Windows; #else @@ -36,47 +36,47 @@ namespace StardewModdingAPI #endif /// The full path to the Stardew Valley executable. - private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, Program.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + private readonly string GameExecutablePath; /// The full path to the folder containing mods. - private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); + private readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); /// The log file to which to write messages. - private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); + private readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); /// Manages console output interception. - private static readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); /// The core logger for SMAPI. - private static readonly Monitor Monitor = new Monitor("SMAPI", Program.ConsoleManager, Program.LogFile); + private readonly Monitor Monitor; /// The user settings for SMAPI. - private static UserSettings Settings; + private UserSettings Settings; /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. - private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); /// Whether the game is currently running. - private static bool ready; + private bool IsGameRunning; /********* ** Accessors *********/ /// The underlying game instance. - internal static SGame GameInstance; + internal SGame GameInstance; /// The number of mods currently loaded by SMAPI. - internal static int ModsLoaded; + internal int ModsLoaded; /// Tracks the installed mods. - internal static readonly ModRegistry ModRegistry = new ModRegistry(); + internal readonly ModRegistry ModRegistry = new ModRegistry(); /// Manages deprecation warnings. - internal static readonly DeprecationManager DeprecationManager = new DeprecationManager(Program.Monitor, Program.ModRegistry); + internal readonly DeprecationManager DeprecationManager; /// Manages console commands. - internal static readonly CommandManager CommandManager = new CommandManager(); + internal readonly CommandManager CommandManager = new CommandManager(); /********* @@ -85,11 +85,36 @@ namespace StardewModdingAPI /// The main entry point which hooks into and launches the game. /// The command-line arguments. private static void Main(string[] args) + { + new Program(writeToConsole: !args.Contains("--no-terminal")) + .LaunchInteractively(); + } + + /// Construct an instance. + internal Program(bool writeToConsole) + { + this.GameExecutablePath = Path.Combine(Constants.ExecutionPath, this.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; + this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + } + + /// Launch SMAPI. + internal void LaunchInteractively() { // initialise logging - Program.Monitor.WriteToConsole = !args.Contains("--no-terminal"); Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - Program.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); + + // 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); + Mod.Shim(this.DeprecationManager); + PlayerEvents.Shim(this.DeprecationManager); + TimeEvents.Shim(this.DeprecationManager); +#pragma warning restore 618 // read config { @@ -97,39 +122,39 @@ namespace StardewModdingAPI if (File.Exists(settingsPath)) { string json = File.ReadAllText(settingsPath); - Program.Settings = JsonConvert.DeserializeObject(json); + this.Settings = JsonConvert.DeserializeObject(json); } else - Program.Settings = new UserSettings(); + this.Settings = new UserSettings(); - File.WriteAllText(settingsPath, JsonConvert.SerializeObject(Program.Settings, Formatting.Indented)); + File.WriteAllText(settingsPath, JsonConvert.SerializeObject(this.Settings, Formatting.Indented)); } // redirect direct console output { - Monitor monitor = Program.GetSecondaryMonitor("Console.Out"); + Monitor monitor = this.GetSecondaryMonitor("Console.Out"); monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion if (monitor.WriteToConsole) - Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); + this.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); } // add warning headers - if (Program.Settings.DeveloperMode) + if (this.Settings.DeveloperMode) { - Program.Monitor.ShowTraceInConsole = true; - Program.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 or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + 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 or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); } - if (!Program.Settings.CheckForUpdates) - Program.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 editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); - if (!Program.Monitor.WriteToConsole) - Program.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", 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 editing or deleting {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); // print file paths - Program.Monitor.Log($"Mods go here: {Program.ModPath}"); + this.Monitor.Log($"Mods go here: {this.ModPath}"); // initialise legacy log - Log.Monitor = Program.GetSecondaryMonitor("legacy mod"); - Log.ModRegistry = Program.ModRegistry; + Log.Monitor = this.GetSecondaryMonitor("legacy mod"); + Log.ModRegistry = this.ModRegistry; // hook into & launch the game try @@ -137,56 +162,56 @@ namespace StardewModdingAPI // verify version if (String.Compare(Game1.version, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) { - Program.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); return; } // initialise - Program.Monitor.Log("Loading SMAPI..."); - Console.Title = Constants.ConsoleTitle; - Program.VerifyPath(Program.ModPath); - Program.VerifyPath(Constants.LogDir); - if (!File.Exists(Program.GameExecutablePath)) + this.Monitor.Log("Loading SMAPI..."); + Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion}"; + this.VerifyPath(this.ModPath); + this.VerifyPath(Constants.LogDir); + if (!File.Exists(this.GameExecutablePath)) { - Program.Monitor.Log($"Couldn't find executable: {Program.GameExecutablePath}", LogLevel.Error); - Program.PressAnyKeyToExit(); + this.Monitor.Log($"Couldn't find executable: {this.GameExecutablePath}", LogLevel.Error); + this.PressAnyKeyToExit(); return; } // check for update when game loads - if (Program.Settings.CheckForUpdates) - GameEvents.GameLoaded += (sender, e) => Program.CheckForUpdateAsync(); + if (this.Settings.CheckForUpdates) + GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); // launch game - Program.StartGame(); + this.StartGame(); } catch (Exception ex) { - Program.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error); } - Program.PressAnyKeyToExit(); + this.PressAnyKeyToExit(); } /// 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. /// The module which requested an immediate exit. /// The reason provided for the shutdown. - internal static void ExitGameImmediately(string module, string reason) + internal void ExitGameImmediately(string module, string reason) { - Program.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}"); - Program.CancellationTokenSource.Cancel(); - if (Program.ready) + this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}"); + this.CancellationTokenSource.Cancel(); + if (this.IsGameRunning) { - Program.GameInstance.Exiting += (sender, e) => Program.PressAnyKeyToExit(); - Program.GameInstance.Exit(); + this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit(); + this.GameInstance.Exit(); } } /// Get a monitor for legacy code which doesn't have one passed in. [Obsolete("This method should only be used when needed for backwards compatibility.")] - internal static IMonitor GetLegacyMonitorForMod() + internal IMonitor GetLegacyMonitorForMod() { - string modName = Program.ModRegistry.GetModFromStack() ?? "unknown"; - return Program.GetSecondaryMonitor(modName); + string modName = this.ModRegistry.GetModFromStack() ?? "unknown"; + return this.GetSecondaryMonitor(modName); } @@ -194,7 +219,7 @@ namespace StardewModdingAPI ** Private methods *********/ /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. - private static void CheckForUpdateAsync() + private void CheckForUpdateAsync() { new Thread(() => { @@ -203,40 +228,40 @@ namespace StardewModdingAPI GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; ISemanticVersion latestVersion = new SemanticVersion(release.Tag); if (latestVersion.IsNewerThan(Constants.ApiVersion)) - Program.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); + this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); } catch (Exception ex) { - Program.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); + this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); } }).Start(); } /// Hook into Stardew Valley and launch the game. - private static void StartGame() + private void StartGame() { try { // add error handlers #if SMAPI_FOR_WINDOWS - Application.ThreadException += (sender, e) => Program.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); + 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) => Program.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // initialise game { // load assembly - Program.Monitor.Log("Loading game..."); - Assembly gameAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); + this.Monitor.Log("Loading game..."); + Assembly gameAssembly = Assembly.UnsafeLoadFrom(this.GameExecutablePath); Type gameProgramType = gameAssembly.GetType("StardewValley.Program", true); // set Game1 instance - Program.GameInstance = new SGame(Program.Monitor); - Program.GameInstance.Exiting += (sender, e) => Program.ready = false; - Program.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e); - Program.GameInstance.Window.Title = $"Stardew Valley - Version {Game1.version}"; - gameProgramType.GetField("gamePtr").SetValue(gameProgramType, Program.GameInstance); + 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 - Version {Game1.version}"; + gameProgramType.GetField("gamePtr").SetValue(gameProgramType, this.GameInstance); // configure Game1.version += $" | SMAPI {Constants.ApiVersion}"; @@ -244,10 +269,10 @@ namespace StardewModdingAPI } // load mods - Program.LoadMods(); - if (Program.CancellationTokenSource.IsCancellationRequested) + this.LoadMods(); + if (this.CancellationTokenSource.IsCancellationRequested) { - Program.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); + this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); return; } @@ -255,18 +280,18 @@ namespace StardewModdingAPI new Thread(() => { // wait for the game to load up - while (!Program.ready) + while (!this.IsGameRunning) Thread.Sleep(1000); // register help command - Program.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", Program.HandleHelpCommand); + this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", this.HandleHelpCommand); // listen for command line input - Program.Monitor.Log("Starting console..."); - Program.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - Thread consoleInputThread = new Thread(Program.ConsoleInputLoop); + this.Monitor.Log("Starting console..."); + this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); + Thread consoleInputThread = new Thread(this.ConsoleInputLoop); consoleInputThread.Start(); - while (Program.ready) + 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 @@ -275,31 +300,31 @@ namespace StardewModdingAPI }).Start(); // start game loop - Program.Monitor.Log("Starting game..."); - if (Program.CancellationTokenSource.IsCancellationRequested) + this.Monitor.Log("Starting game..."); + if (this.CancellationTokenSource.IsCancellationRequested) { - Program.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); + this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); return; } try { - Program.ready = true; - Program.GameInstance.Run(); + this.IsGameRunning = true; + this.GameInstance.Run(); } finally { - Program.ready = false; + this.IsGameRunning = false; } } catch (Exception ex) { - Program.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error); } } /// Create a directory path if it doesn't exist. /// The directory path. - private static void VerifyPath(string path) + private void VerifyPath(string path) { try { @@ -308,20 +333,20 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); } } /// Load and hook up all mods in the mod directory. - private static void LoadMods() + private void LoadMods() { - Program.Monitor.Log("Loading mods..."); + this.Monitor.Log("Loading mods..."); // get JSON helper JsonHelper jsonHelper = new JsonHelper(); // get assembly loader - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Program.TargetPlatform, Program.Monitor); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(this.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // get known incompatible mods @@ -335,12 +360,12 @@ namespace StardewModdingAPI catch (Exception ex) { incompatibleMods = new Dictionary(); - Program.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn); + this.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn); } // load mod assemblies List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list - foreach (string directoryPath in Directory.GetDirectories(Program.ModPath)) + foreach (string directoryPath in Directory.GetDirectories(this.ModPath)) { // passthrough empty directories DirectoryInfo directory = new DirectoryInfo(directoryPath); @@ -348,9 +373,9 @@ namespace StardewModdingAPI directory = directory.GetDirectories().First(); // check for cancellation - if (Program.CancellationTokenSource.IsCancellationRequested) + if (this.CancellationTokenSource.IsCancellationRequested) { - Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); + this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); return; } @@ -358,7 +383,7 @@ namespace StardewModdingAPI string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { - Program.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); + this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; @@ -371,7 +396,7 @@ namespace StardewModdingAPI string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { - Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); continue; } @@ -379,18 +404,18 @@ namespace StardewModdingAPI manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json"), null); if (manifest == null) { - Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); continue; } if (string.IsNullOrEmpty(manifest.EntryDll)) { - Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -407,7 +432,7 @@ namespace StardewModdingAPI if (!string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl)) warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - Program.Monitor.Log(warning, LogLevel.Error); + this.Monitor.Log(warning, LogLevel.Error); continue; } } @@ -420,13 +445,13 @@ namespace StardewModdingAPI ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { - Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); continue; } } catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { - Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); continue; } } @@ -434,20 +459,20 @@ namespace StardewModdingAPI // create per-save directory if (manifest.PerSaveConfigs) { - deprecationWarnings.Add(() => Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); + deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); try { string psDir = Path.Combine(directory.FullName, "psconfigs"); Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { - Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } @@ -456,7 +481,7 @@ namespace StardewModdingAPI string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { - Program.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); continue; } @@ -468,7 +493,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -477,13 +502,13 @@ namespace StardewModdingAPI { if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) { - Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -496,25 +521,25 @@ namespace StardewModdingAPI mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { - Program.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated."); + this.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated."); continue; } // inject data // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, Program.ModRegistry, Program.CommandManager); - mod.Monitor = Program.GetSecondaryMonitor(manifest.Name); + mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager); + mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; // track mod - Program.ModRegistry.Add(mod); - Program.ModsLoaded += 1; - Program.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); + this.ModRegistry.Add(mod); + this.ModsLoaded += 1; + this.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } @@ -525,7 +550,7 @@ namespace StardewModdingAPI deprecationWarnings = null; // initialise mods - foreach (Mod mod in Program.ModRegistry.GetMods()) + foreach (Mod mod in this.ModRegistry.GetMods()) { try { @@ -534,54 +559,54 @@ namespace StardewModdingAPI mod.Entry(mod.Helper); // raise deprecation warning for old Entry() methods - if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - Program.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); + if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) + this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); } catch (Exception ex) { - Program.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); + this.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); } } // print result - Program.Monitor.Log($"Loaded {Program.ModsLoaded} mods."); - Console.Title = Constants.ConsoleTitle; + this.Monitor.Log($"Loaded {this.ModsLoaded} mods."); + Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {this.ModsLoaded}"; } // ReSharper disable once FunctionNeverReturns /// Run a loop handling console input. - private static void ConsoleInputLoop() + private void ConsoleInputLoop() { while (true) { string input = Console.ReadLine(); - if (!Program.CommandManager.Trigger(input)) - Program.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + if (!this.CommandManager.Trigger(input)) + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); } } /// The method called when the user submits the help command in the console. /// The command name. /// The command arguments. - private static void HandleHelpCommand(string name, string[] arguments) + private void HandleHelpCommand(string name, string[] arguments) { if (arguments.Any()) { - Framework.Command result = Program.CommandManager.Get(arguments[0]); + Framework.Command result = this.CommandManager.Get(arguments[0]); if (result == null) - Program.Monitor.Log("There's no command with that name.", LogLevel.Error); + this.Monitor.Log("There's no command with that name.", LogLevel.Error); else - Program.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); + this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); } else - Program.Monitor.Log("Commands: " + string.Join(", ", Program.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); + this.Monitor.Log("Commands: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); } /// Show a 'press any key to exit' message, and exit when they press a key. - private static void PressAnyKeyToExit() + private void PressAnyKeyToExit() { - Program.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); @@ -589,9 +614,9 @@ namespace StardewModdingAPI /// Get a monitor instance derived from SMAPI's current settings. /// The name of the module which will log messages with this instance. - private static Monitor GetSecondaryMonitor(string name) + private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, Program.ConsoleManager, Program.LogFile) { WriteToConsole = Program.Monitor.WriteToConsole, ShowTraceInConsole = Program.Settings.DeveloperMode }; + return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode }; } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 1e896d4b..35dd6513 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -151,6 +151,7 @@ + -- cgit