From 176eddbf7b70934c2665aa3a0ac8b46bef04012a Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
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')

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
-        ****/
         /// <summary>The commands registered with SMAPI.</summary>
         private static readonly IDictionary<string, Command> LegacyCommands = new Dictionary<string, Command>(StringComparer.InvariantCultureIgnoreCase);
 
+        /// <summary>Manages console commands.</summary>
+        private static CommandManager CommandManager;
+
+        /// <summary>Manages deprecation warnings.</summary>
+        private static DeprecationManager DeprecationManager;
+
+        /// <summary>Tracks the installed mods.</summary>
+        private static ModRegistry ModRegistry;
+
+
+        /*********
+        ** Accessors
+        *********/
         /// <summary>The event raised when this command is submitted through the console.</summary>
         public event EventHandler<EventArgsCommand> CommandFired;
 
@@ -43,6 +53,17 @@ namespace StardewModdingAPI
         /****
         ** Command
         ****/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="commandManager">Manages console commands.</param>
+        /// <param name="deprecationManager">Manages deprecation warnings.</param>
+        /// <param name="modRegistry">Tracks the installed mods.</param>
+        internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry)
+        {
+            Command.CommandManager = commandManager;
+            Command.DeprecationManager = deprecationManager;
+            Command.ModRegistry = modRegistry;
+        }
+
         /// <summary>Construct an instance.</summary>
         /// <param name="name">The name of the command.</param>
         /// <param name="description">A human-readable description of what the command does.</param>
@@ -73,8 +94,8 @@ namespace StardewModdingAPI
         /// <param name="monitor">Encapsulates monitoring and logging.</param>
         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);
         }
 
         /// <summary>Register a command with SMAPI.</summary>
@@ -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() ?? "<unknown mod>";
+            string modName = Command.ModRegistry.GetModFromStack() ?? "<unknown mod>";
             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
         /// <param name="name">The command name to find.</param>
         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
+        *********/
+        /// <summary>Manages deprecation warnings.</summary>
+        private static DeprecationManager DeprecationManager;
+
+
         /*********
         ** Accessors
         *********/
@@ -26,6 +33,13 @@ namespace StardewModdingAPI
         /*********
         ** Public methods
         *********/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="deprecationManager">Manages deprecation warnings.</param>
+        internal static void Shim(DeprecationManager deprecationManager)
+        {
+            Config.DeprecationManager = deprecationManager;
+        }
+
         /// <summary>Construct an instance of the config class.</summary>
         /// <typeparam name="T">The config class type.</typeparam>
         [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")]
@@ -111,8 +125,8 @@ namespace StardewModdingAPI
         /// <summary>Construct an instance.</summary>
         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
         /// <summary>The GitHub repository to check for updates.</summary>
         internal const string GitHubRepository = "Pathoschild/SMAPI";
 
-        /// <summary>The title of the SMAPI console window.</summary>
-        internal static string ConsoleTitle => $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {Program.ModsLoaded}";
-
         /// <summary>The file path for the SMAPI configuration file.</summary>
         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
     /// <summary>Events raised when the player data changes.</summary>
     public static class PlayerEvents
     {
+        /*********
+        ** Properties
+        *********/
+        /// <summary>Manages deprecation warnings.</summary>
+        private static DeprecationManager DeprecationManager;
+
+
         /*********
         ** Events
         *********/
@@ -31,6 +38,13 @@ namespace StardewModdingAPI.Events
         /*********
         ** Internal methods
         *********/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="deprecationManager">Manages deprecation warnings.</param>
+        internal static void Shim(DeprecationManager deprecationManager)
+        {
+            PlayerEvents.DeprecationManager = deprecationManager;
+        }
+
         /// <summary>Raise a <see cref="LoadedGame"/> event.</summary>
         /// <param name="monitor">Encapsulates monitoring and logging.</param>
         /// <param name="loaded">Whether the save has been loaded. This is always true.</param>
@@ -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
     /// <summary>Events raised when the in-game date or time changes.</summary>
     public static class TimeEvents
     {
+        /*********
+        ** Properties
+        *********/
+        /// <summary>Manages deprecation warnings.</summary>
+        private static DeprecationManager DeprecationManager;
+
+
         /*********
         ** Events
         *********/
@@ -32,6 +39,13 @@ namespace StardewModdingAPI.Events
         /*********
         ** Internal methods
         *********/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="deprecationManager">Manages deprecation warnings.</param>
+        internal static void Shim(DeprecationManager deprecationManager)
+        {
+            TimeEvents.DeprecationManager = deprecationManager;
+        }
+
         /// <summary>Raise an <see cref="AfterDayStarted"/> event.</summary>
         /// <param name="monitor">Encapsulates monitoring and logging.</param>
         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
     /// <summary>Provides extension methods for SMAPI's internal use.</summary>
     internal static class InternalExtensions
     {
+        /*********
+        ** Properties
+        *********/
+        /// <summary>Tracks the installed mods.</summary>
+        private static ModRegistry ModRegistry;
+
+
         /*********
         ** Public methods
         *********/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="modRegistry">Tracks the installed mods.</param>
+        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
         };
 
+        /// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
+        private RequestExitDelegate RequestExit;
+
 
         /*********
         ** Accessors
@@ -55,7 +58,8 @@ namespace StardewModdingAPI.Framework
         /// <param name="source">The name of the module which logs messages using this instance.</param>
         /// <param name="consoleManager">Manages access to the console output.</param>
         /// <param name="logFile">The log file to which to write messages.</param>
-        public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile)
+        /// <param name="requestExitDelegate">A delegate which requests that SMAPI immediately exit the game.</param>
+        public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, RequestExitDelegate requestExitDelegate)
         {
             // validate
             if (string.IsNullOrWhiteSpace(source))
@@ -81,8 +85,7 @@ namespace StardewModdingAPI.Framework
         /// <param name="reason">The reason for the shutdown.</param>
         public void ExitGameImmediately(string reason)
         {
-            Program.ExitGameImmediately(this.Source, reason);
-            Program.GameInstance.Exit();
+            this.RequestExit(this.Source, reason);
         }
 
         /// <summary>Log a fatal error message.</summary>
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
+{
+    /// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
+    /// <param name="module">The module which requested an immediate exit.</param>
+    /// <param name="reason">The reason provided for the shutdown.</param>
+    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
+        *********/
+        /// <summary>Manages deprecation warnings.</summary>
+        private static DeprecationManager DeprecationManager;
+
+
         /*********
         ** Accessors
         *********/
@@ -22,6 +29,13 @@ namespace StardewModdingAPI
         /*********
         ** Public methods
         *********/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="deprecationManager">Manages deprecation warnings.</param>
+        internal static void Shim(DeprecationManager deprecationManager)
+        {
+            Log.DeprecationManager = deprecationManager;
+        }
+
         /****
         ** Exceptions
         ****/
@@ -292,7 +306,7 @@ namespace StardewModdingAPI
         /// <summary>Raise a deprecation warning.</summary>
         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);
         }
 
         /// <summary>Get the name of the mod logging a message from the stack.</summary>
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
         *********/
+        /// <summary>Manages deprecation warnings.</summary>
+        private static DeprecationManager DeprecationManager;
+
         /// <summary>The backing field for <see cref="Mod.PathOnDisk"/>.</summary>
         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
         *********/
+        /// <summary>Injects types required for backwards compatibility.</summary>
+        /// <param name="deprecationManager">Manages deprecation warnings.</param>
+        internal static void Shim(DeprecationManager deprecationManager)
+        {
+            Mod.DeprecationManager = deprecationManager;
+        }
+
         /// <summary>The mod entry point, called after the mod is first loaded.</summary>
         [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
         *********/
         /// <summary>The target game platform.</summary>
-        private static readonly Platform TargetPlatform =
+        private readonly Platform TargetPlatform =
 #if SMAPI_FOR_WINDOWS
         Platform.Windows;
 #else
@@ -36,47 +36,47 @@ namespace StardewModdingAPI
 #endif
 
         /// <summary>The full path to the Stardew Valley executable.</summary>
-        private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, Program.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe");
+        private readonly string GameExecutablePath;
 
         /// <summary>The full path to the folder containing mods.</summary>
-        private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods");
+        private readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods");
 
         /// <summary>The log file to which to write messages.</summary>
-        private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath);
+        private readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath);
 
         /// <summary>Manages console output interception.</summary>
-        private static readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager();
+        private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager();
 
         /// <summary>The core logger for SMAPI.</summary>
-        private static readonly Monitor Monitor = new Monitor("SMAPI", Program.ConsoleManager, Program.LogFile);
+        private readonly Monitor Monitor;
 
         /// <summary>The user settings for SMAPI.</summary>
-        private static UserSettings Settings;
+        private UserSettings Settings;
 
         /// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary>
-        private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
+        private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
 
         /// <summary>Whether the game is currently running.</summary>
-        private static bool ready;
+        private bool IsGameRunning;
 
 
         /*********
         ** Accessors
         *********/
         /// <summary>The underlying game instance.</summary>
-        internal static SGame GameInstance;
+        internal SGame GameInstance;
 
         /// <summary>The number of mods currently loaded by SMAPI.</summary>
-        internal static int ModsLoaded;
+        internal int ModsLoaded;
 
         /// <summary>Tracks the installed mods.</summary>
-        internal static readonly ModRegistry ModRegistry = new ModRegistry();
+        internal readonly ModRegistry ModRegistry = new ModRegistry();
 
         /// <summary>Manages deprecation warnings.</summary>
-        internal static readonly DeprecationManager DeprecationManager = new DeprecationManager(Program.Monitor, Program.ModRegistry);
+        internal readonly DeprecationManager DeprecationManager;
 
         /// <summary>Manages console commands.</summary>
-        internal static readonly CommandManager CommandManager = new CommandManager();
+        internal readonly CommandManager CommandManager = new CommandManager();
 
 
         /*********
@@ -85,11 +85,36 @@ 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)
+        {
+            new Program(writeToConsole: !args.Contains("--no-terminal"))
+                .LaunchInteractively();
+        }
+
+        /// <summary>Construct an instance.</summary>
+        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);
+        }
+
+        /// <summary>Launch SMAPI.</summary>
+        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<UserSettings>(json);
+                    this.Settings = JsonConvert.DeserializeObject<UserSettings>(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();
         }
 
         /// <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 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();
             }
         }
 
         /// <summary>Get a monitor for legacy code which doesn't have one passed in.</summary>
         [Obsolete("This method should only be used when needed for backwards compatibility.")]
-        internal 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
         *********/
         /// <summary>Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available.</summary>
-        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();
         }
 
         /// <summary>Hook into Stardew Valley and launch the game.</summary>
-        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 <cmd>' returns command description", Program.HandleHelpCommand);
+                    this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help <cmd>' returns command description", this.HandleHelpCommand);
 
                     // listen for command line input
-                    Program.Monitor.Log("Starting console...");
-                    Program.Monitor.Log("Type 'help' for help, or 'help <cmd>' 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 <cmd>' 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);
             }
         }
 
         /// <summary>Create a directory path if it doesn't exist.</summary>
         /// <param name="path">The directory path.</param>
-        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);
             }
         }
 
         /// <summary>Load and hook up all mods in the mod directory.</summary>
-        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<string, IncompatibleMod>();
-                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<Action> deprecationWarnings = new List<Action>(); // 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<Manifest>(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
         /// <summary>Run a loop handling console input.</summary>
-        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);
             }
         }
 
         /// <summary>The method called when the user submits the help command in the console.</summary>
         /// <param name="name">The command name.</param>
         /// <param name="arguments">The command arguments.</param>
-        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);
         }
 
         /// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
-        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
 
         /// <summary>Get a monitor instance derived from SMAPI's current settings.</summary>
         /// <param name="name">The name of the module which will log messages with this instance.</param>
-        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 @@
     <Compile Include="Framework\Logging\InterceptingTextWriter.cs" />
     <Compile Include="Framework\CommandHelper.cs" />
     <Compile Include="Framework\Reflection\PrivateProperty.cs" />
+    <Compile Include="Framework\RequestExitDelegate.cs" />
     <Compile Include="Framework\Serialisation\JsonHelper.cs" />
     <Compile Include="Framework\Serialisation\SemanticVersionConverter.cs" />
     <Compile Include="ICommandHelper.cs" />
-- 
cgit