From 417c04076634ea87d7b3030a1acf46825da6e3e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 01:53:35 -0400 Subject: add data API (#468) --- src/SMAPI/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 634c5066..c40d2ff6 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -865,6 +865,7 @@ namespace StardewModdingAPI IModEvents events = new ModEvents(metadata, this.EventManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.GameInstance.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IDataHelper dataHelper = new DataHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); @@ -877,7 +878,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod -- cgit From 826dd53ab550e5b92796c510569118beee6bd044 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 18:28:16 -0400 Subject: move most SMAPI files into subfolder (#582) --- src/SMAPI/Program.cs | 85 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 28 deletions(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c40d2ff6..64eeb45a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -45,6 +45,11 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// The absolute path to search for SMAPI's internal DLLs. + /// We can't use directly, since depends on DLLs loaded from this folder. + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + internal static readonly string DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal"); + /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -111,6 +116,8 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { + // initial setup + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; Program.AssertMinimumCompatibility(); // get flags from arguments @@ -135,10 +142,48 @@ namespace StardewModdingAPI program.RunInteractively(); } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // skip if already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; + this.Monitor.Log("Disposing...", LogLevel.Trace); + + // dispose mod data + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { + try + { + (mod.Mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } + + // dispose core components + this.IsGameRunning = false; + this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); + this.CancellationTokenSource?.Dispose(); + this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); + + // end game (moved from Game1.OnExiting to let us clean up first) + Process.GetCurrentProcess().Kill(); + } + + + /********* + ** Private methods + *********/ /// Construct an instance. /// The path to search for mods. /// Whether to output log messages to the console. - public Program(string modsPath, bool writeToConsole) + private Program(string modsPath, bool writeToConsole) { // init paths this.VerifyPath(modsPath); @@ -189,7 +234,7 @@ namespace StardewModdingAPI /// Launch SMAPI. [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - public void RunInteractively() + private void RunInteractively() { // initialise SMAPI try @@ -320,44 +365,28 @@ namespace StardewModdingAPI } } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + /// Method called when assembly resolution fails, which may return a manually resolved assembly. + /// The event sender. + /// The event arguments. + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { - // skip if already disposed - if (this.IsDisposed) - return; - this.IsDisposed = true; - this.Monitor.Log("Disposing...", LogLevel.Trace); - - // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { try { - (mod.Mod as IDisposable)?.Dispose(); + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + return Assembly.LoadFrom(dll.FullName); } catch (Exception ex) { - mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting the smapi-lib folder and reinstalling SMAPI.", ex); } } - // dispose core components - this.IsGameRunning = false; - this.ConsoleManager?.Dispose(); - this.ContentCore?.Dispose(); - this.CancellationTokenSource?.Dispose(); - this.GameInstance?.Dispose(); - this.LogFile?.Dispose(); - - // end game (moved from Game1.OnExiting to let us clean up first) - Process.GetCurrentProcess().Kill(); + return null; } - - /********* - ** Private methods - *********/ /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. private static void AssertMinimumCompatibility() { -- cgit From 464d6b2275f52b9db5560e242c6073fbd816bcb3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 23:41:03 -0400 Subject: fix error handling when resolving assemblies (#582) --- src/SMAPI/Program.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 64eeb45a..f5a32b4f 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -370,21 +370,21 @@ namespace StardewModdingAPI /// The event arguments. private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { - AssemblyName name = new AssemblyName(e.Name); - foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) + try { - try + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) return Assembly.LoadFrom(dll.FullName); } - catch (Exception ex) - { - throw new InvalidOperationException($"Could not load dependency 'smapi-lib/{dll.Name}'. Consider deleting the smapi-lib folder and reinstalling SMAPI.", ex); - } + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error resolving assembly: {ex}"); + return null; } - - return null; } /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. -- cgit From a4ecb49a44559f2ca9c3f00049a1c8a7862cc0c9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 01:44:38 -0400 Subject: update for Stardew Valley 1.3.29 beta (#585) --- src/SMAPI/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index f5a32b4f..b39077c1 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -98,8 +98,8 @@ namespace StardewModdingAPI new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^Multiplayer auth success$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) + new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), }; /// The mod toolkit used for generic mod interactions. @@ -275,7 +275,7 @@ namespace StardewModdingAPI // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler -- cgit From 6443fb12317932c749730fa42457b7c3295cebf4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Aug 2018 02:24:30 -0400 Subject: fix deprecated Read/WriteJsonFiles method enforcing newer restrictions (#468) --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b39077c1..d34cbab5 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -907,7 +907,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // init mod -- cgit From 788f7ae3b7dc37d3323d83830cfeb92bea958e66 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Aug 2018 01:59:31 -0400 Subject: split core logic out of Program (#582) This is needed because Mono validates Program's instance fields before the static Main runs, so the custom assembly resolution isn't set up until the app has already crashed due to invalid property types. --- src/SMAPI/Program.cs | 1281 +------------------------------------------------- 1 file changed, 20 insertions(+), 1261 deletions(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d34cbab5..c6f39e3f 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -1,46 +1,17 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net; using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Security; -using System.Text; -using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS -using System.Windows.Forms; #endif -using Newtonsoft.Json; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.ModHelpers; -using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Toolkit; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; -using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Utilities; -using StardewValley; -using Monitor = StardewModdingAPI.Framework.Monitor; -using SObject = StardewValley.Object; -using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI { /// The main entry point for SMAPI, responsible for hooking into and launching the game. - internal class Program : IDisposable + internal class Program { /********* ** Properties @@ -50,64 +21,6 @@ namespace StardewModdingAPI [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] internal static readonly string DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal"); - /// The log file to which to write messages. - private readonly LogFileManager LogFile; - - /// Manages console output interception. - private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); - - /// The core logger and monitor for SMAPI. - private readonly Monitor Monitor; - - /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. - private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); - - /// Simplifies access to private game code. - private readonly Reflector Reflection = new Reflector(); - - /// The SMAPI configuration settings. - private readonly SConfig Settings; - - /// The underlying game instance. - private SGame GameInstance; - - /// The underlying content manager. - private ContentCoordinator ContentCore => this.GameInstance.ContentCore; - - /// Tracks the installed mods. - /// This is initialised after the game starts. - private readonly ModRegistry ModRegistry = new ModRegistry(); - - /// Manages deprecation warnings. - /// This is initialised after the game starts. - private DeprecationManager DeprecationManager; - - /// Manages SMAPI events for mods. - private readonly EventManager EventManager; - - /// Whether the game is currently running. - private bool IsGameRunning; - - /// Whether the program has been disposed. - private bool IsDisposed; - - /// Regex patterns which match console messages to suppress from the console and log. - private readonly Regex[] SuppressConsolePatterns = - { - new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - }; - - /// The mod toolkit used for generic mod interactions. - private readonly ModToolkit Toolkit = new ModToolkit(); - - /// The path to search for mods. - private readonly string ModsPath; - /********* ** Public methods @@ -116,255 +29,15 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { - // initial setup AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; Program.AssertMinimumCompatibility(); - - // get flags from arguments - bool writeToConsole = !args.Contains("--no-terminal"); - - // get mods path from arguments - string modsPath = null; - { - int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - { - modsPath = args[pathIndex]; - if (!string.IsNullOrWhiteSpace(modsPath) && !Path.IsPathRooted(modsPath)) - modsPath = Path.Combine(Constants.ExecutionPath, modsPath); - } - if (string.IsNullOrWhiteSpace(modsPath)) - modsPath = Constants.DefaultModsPath; - } - - // load SMAPI - using (Program program = new Program(modsPath, writeToConsole)) - program.RunInteractively(); - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - // skip if already disposed - if (this.IsDisposed) - return; - this.IsDisposed = true; - this.Monitor.Log("Disposing...", LogLevel.Trace); - - // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetAll()) - { - try - { - (mod.Mod as IDisposable)?.Dispose(); - } - catch (Exception ex) - { - mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); - } - } - - // dispose core components - this.IsGameRunning = false; - this.ConsoleManager?.Dispose(); - this.ContentCore?.Dispose(); - this.CancellationTokenSource?.Dispose(); - this.GameInstance?.Dispose(); - this.LogFile?.Dispose(); - - // end game (moved from Game1.OnExiting to let us clean up first) - Process.GetCurrentProcess().Kill(); + Program.Start(args); } /********* ** Private methods *********/ - /// Construct an instance. - /// The path to search for mods. - /// Whether to output log messages to the console. - private Program(string modsPath, bool writeToConsole) - { - // init paths - this.VerifyPath(modsPath); - this.VerifyPath(Constants.LogDir); - this.ModsPath = modsPath; - - // init log file - this.PurgeLogFiles(); - string logPath = this.GetLogPath(); - - // init basics - this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); - this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) - { - WriteToConsole = writeToConsole, - ShowTraceInConsole = this.Settings.DeveloperMode, - ShowFullStampInConsole = this.Settings.DeveloperMode - }; - this.EventManager = new EventManager(this.Monitor, this.ModRegistry); - - // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); - this.Monitor.Log($"Mods go here: {modsPath}"); - if (modsPath != Constants.DefaultModsPath) - this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); - this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); - - // validate game version - if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) - { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) - { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - - // apply game patches - new GamePatcher(this.Monitor).Apply( - // new GameLocationPatch() - ); - } - - /// Launch SMAPI. - [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - private void RunInteractively() - { - // initialise SMAPI - try - { - // hook up events - ContentEvents.Init(this.EventManager); - ControlEvents.Init(this.EventManager); - GameEvents.Init(this.EventManager); - GraphicsEvents.Init(this.EventManager); - InputEvents.Init(this.EventManager); - LocationEvents.Init(this.EventManager); - MenuEvents.Init(this.EventManager); - MineEvents.Init(this.EventManager); - MultiplayerEvents.Init(this.EventManager); - PlayerEvents.Init(this.EventManager); - SaveEvents.Init(this.EventManager); - SpecialisedEvents.Init(this.EventManager); - TimeEvents.Init(this.EventManager); - - // init JSON parser - JsonConverter[] converters = { - new ColorConverter(), - new PointConverter(), - new RectangleConverter() - }; - foreach (JsonConverter converter in converters) - this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); - - // add error handlers -#if SMAPI_FOR_WINDOWS - Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); -#endif - AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - - // add more leniant assembly resolvers - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); - - // override game - SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose); - StardewValley.Program.gamePtr = this.GameInstance; - - // add exit handler - new Thread(() => - { - this.CancellationTokenSource.Token.WaitHandle.WaitOne(); - if (this.IsGameRunning) - { - try - { - File.WriteAllText(Constants.FatalCrashMarker, string.Empty); - File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); - } - - this.GameInstance.Exit(); - } - }).Start(); - - // hook into game events - ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); - - // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - - // check update marker - if (File.Exists(Constants.UpdateMarker)) - { - string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); - if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) - { - if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) - { - this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); - this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); - this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); - Console.ReadKey(); - } - } - File.Delete(Constants.UpdateMarker); - } - - // show details if game crashed during last session - if (File.Exists(Constants.FatalCrashMarker)) - { - this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: http://community.playstarbound.com/threads/108375/.", LogLevel.Error); - this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error); - this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); - Console.ReadKey(); - File.Delete(Constants.FatalCrashLog); - File.Delete(Constants.FatalCrashMarker); - } - - // start game - this.Monitor.Log("Starting game...", LogLevel.Debug); - try - { - this.IsGameRunning = true; - StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window - this.GameInstance.Run(); - } - catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) - { - this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); - this.PressAnyKeyToExit(); - } - catch (Exception ex) - { - this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); - } - finally - { - this.Dispose(); - } - } - /// Method called when assembly resolution fails, which may return a manually resolved assembly. /// The event sender. /// The event arguments. @@ -422,880 +95,31 @@ namespace StardewModdingAPI } } - /// Initialise SMAPI and mods after the game starts. - private void InitialiseAfterGameStart() - { - // load settings - this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; - - // load core components - this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); - - // redirect direct console output - { - Monitor monitor = this.GetSecondaryMonitor("game"); - if (monitor.WriteToConsole) - this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); - } - - // add headers - if (this.Settings.DeveloperMode) - this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - if (!this.Settings.CheckForUpdates) - this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); - if (!this.Monitor.WriteToConsole) - this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - this.VerboseLog("Verbose logging enabled."); - - // validate XNB integrity - if (!this.ValidateContentIntegrity()) - this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); - - // load mod data - ModToolkit toolkit = new ModToolkit(); - ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); - - // load mods - { - this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); - ModResolver resolver = new ModResolver(); - - // load manifests - IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); - - // process dependencies - mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); - - // load mods - this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); - - // write metadata file - if (this.Settings.DumpMetadata) - { - ModFolderExport export = new ModFolderExport - { - Exported = DateTime.UtcNow.ToString("O"), - ApiVersion = Constants.ApiVersion.ToString(), - GameVersion = Constants.GameVersion.ToString(), - ModFolderPath = this.ModsPath, - Mods = mods - }; - this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export); - } - - // check for updates - this.CheckForUpdatesAsync(mods); - } - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); - return; - } - - // update window titles - int modsLoaded = this.ModRegistry.GetAll().Count(); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; - - // start SMAPI console - new Thread(this.RunConsoleLoop).Start(); - } - - /// Handle the game changing locale. - private void OnLocaleChanged() - { - // get locale - string locale = this.ContentCore.GetLocale(); - LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; - - // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) - (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); - } - - /// Run a loop handling console input. - [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] - private void RunConsoleLoop() - { - // prepare console - this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.GameInstance.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); - this.GameInstance.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); - - // start handling command line input - Thread inputThread = new Thread(() => - { - while (true) - { - // get input - string input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) - continue; - - // handle command - this.Monitor.LogUserInput(input); - this.GameInstance.CommandQueue.Enqueue(input); - } - }); - inputThread.Start(); - - // keep console thread alive while the game is running - while (this.IsGameRunning && !this.Monitor.IsExiting) - Thread.Sleep(1000 / 10); - if (inputThread.ThreadState == ThreadState.Running) - inputThread.Abort(); - } - - /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. - /// Returns whether all integrity checks passed. - private bool ValidateContentIntegrity() - { - this.Monitor.Log("Detecting common issues...", LogLevel.Trace); - bool issuesFound = false; - - // object format (commonly broken by outdated files) - { - // detect issues - bool hasObjectIssues = false; - void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace); - foreach (KeyValuePair entry in Game1.objectInformation) - { - // must not be empty - if (string.IsNullOrWhiteSpace(entry.Value)) - { - LogIssue(entry.Key, "entry is empty"); - hasObjectIssues = true; - continue; - } - - // require core fields - string[] fields = entry.Value.Split('/'); - if (fields.Length < SObject.objectInfoDescriptionIndex + 1) - { - LogIssue(entry.Key, "too few fields for an object"); - hasObjectIssues = true; - continue; - } - - // check min length for specific types - switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) - { - case "Cooking": - if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) - { - LogIssue(entry.Key, "too few fields for a cooking item"); - hasObjectIssues = true; - } - break; - } - } - - // log error - if (hasObjectIssues) - { - issuesFound = true; - this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); - } - } - - return !issuesFound; - } - - /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. - /// The mods to include in the update check (if eligible). - private void CheckForUpdatesAsync(IModMetadata[] mods) - { - if (!this.Settings.CheckForUpdates) - return; - - new Thread(() => - { - // create client - string url = this.Settings.WebApiBaseUrl; -#if !SMAPI_FOR_WINDOWS - url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif - WebApiClient client = new WebApiClient(url, Constants.ApiVersion); - this.Monitor.Log("Checking for updates...", LogLevel.Trace); - - // check SMAPI version - ISemanticVersion updateFound = null; - try - { - ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; - ISemanticVersion latestStable = response.Main?.Version; - ISemanticVersion latestBeta = response.Optional?.Version; - - if (latestStable == null && response.Errors.Any()) - { - 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.", LogLevel.Warn); - this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); - } - else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) - { - updateFound = latestBeta; - this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) - { - updateFound = latestStable; - this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else - this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); - } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log(ex is WebException && ex.InnerException == null - ? $"Error: {ex.Message}" - : $"Error: {ex.GetLogSummary()}" - ); - } - - // show update message on next launch - if (updateFound != null) - File.WriteAllText(Constants.UpdateMarker, updateFound.ToString()); - - // check mod versions - if (mods.Any()) - { - try - { - HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - - // prepare search model - List searchMods = new List(); - foreach (IModMetadata mod in mods) - { - if (!mod.HasID()) - continue; - - string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; - searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); - } - - // fetch results - this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); - IDictionary results = client.GetModInfo(searchMods.ToArray()); - - // extract update alerts & errors - var updates = new List>(); - var errors = new StringBuilder(); - foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) - { - // link to update-check data - if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) - continue; - mod.SetUpdateData(result); - - // handle errors - if (result.Errors != null && result.Errors.Any()) - { - errors.AppendLine(result.Errors.Length == 1 - ? $" {mod.DisplayName}: {result.Errors[0]}" - : $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}" - ); - } - - // parse versions - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; - ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; - ISemanticVersion unofficialVersion = result.Unofficial?.Version; - - // show update alerts - if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) - updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); - else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) - updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); - else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) - updates.Add(Tuple.Create(mod, unofficialVersion, result.Unofficial?.Url)); - } - - // show update errors - if (errors.Length != 0) - this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace); - - // show update alerts - if (updates.Any()) - { - this.Monitor.Newline(); - this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updates) - { - IModMetadata mod = entry.Item1; - ISemanticVersion newVersion = entry.Item2; - string newUrl = entry.Item3; - this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); - } - } - else - this.Monitor.Log(" All mods up to date.", LogLevel.Trace); - } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); - this.Monitor.Log(ex is WebException && ex.InnerException == null - ? ex.Message - : ex.ToString() - ); - } - } - }).Start(); - } - - /// Get whether a given version should be offered to the user as an update. - /// The current semantic version. - /// The target semantic version. - /// Whether the user enabled the beta channel and should be offered pre-release updates. - private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) - { - return - newVersion != null - && newVersion.IsNewerThan(currentVersion) - && (useBetaChannel || !newVersion.IsPrerelease()); - } - - /// Create a directory path if it doesn't exist. - /// The directory path. - private void VerifyPath(string path) - { - try - { - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - /// Load and hook up the given mods. - /// The mods to load. - /// The JSON helper with which to read mods' JSON files. - /// The content manager to use for mod content. - /// Handles access to SMAPI's internal mod metadata list. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) - { - this.Monitor.Log("Loading mods...", LogLevel.Trace); - - HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - IDictionary skippedMods = new Dictionary(); - void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase }; - - // load content packs - foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) - { - this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)})...", LogLevel.Trace); - - // show warning for missing update key - if (metadata.HasManifest() && !metadata.HasUpdateKeys()) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod as content pack - IManifest manifest = metadata.Manifest; - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); - metadata.SetMod(contentPack, monitor); - this.ModRegistry.Add(metadata); - } - IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); - - // load mods - { - // get content packs by mod ID - IDictionary contentPacksByModID = - loadedContentPacks - .GroupBy(p => p.Manifest.ContentPackFor.UniqueID, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(metadata => metadata.ContentPack).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // load mods from metadata - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor)) - { - InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) - { - // get basic info - IManifest manifest = metadata.Manifest; - this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid - : $" {metadata.DisplayName}...", LogLevel.Trace); - - // show warnings - if (metadata.HasManifest() && !metadata.HasUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID)) - metadata.SetWarning(ModWarning.NoUpdateKeys); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // load mod - string assemblyPath = metadata.Manifest?.EntryDll != null - ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) - : null; - Assembly modAssembly; - try - { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); - } - catch (IncompatibleInstructionException) // details already in trace logs - { - string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); - - TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}."); - continue; - } - catch (SAssemblyLoadFailedException ex) - { - TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); - continue; - } - catch (Exception ex) - { - TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); - continue; - } - - // initialise mod - try - { - // get mod instance - if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) - continue; - - // get content packs - if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) - contentPacks = new IContentPack[0]; - - // init mod helpers - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IModHelper modHelper; - { - IModEvents events = new ModEvents(metadata, this.EventManager); - ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.GameInstance.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IDataHelper dataHelper = new DataHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); - IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); - - IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) - { - IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); - } - - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); - } - - // init mod - mod.ModManifest = manifest; - mod.Helper = modHelper; - mod.Monitor = monitor; - - // track mod - metadata.SetMod(mod); - this.ModRegistry.Add(metadata); - } - catch (Exception ex) - { - TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); - } - } - } - } - IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); - - // log loaded mods - this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); - foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - - // log loaded content packs - if (loadedContentPacks.Any()) - { - string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; - - this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); - foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - } - - // log mod warnings - this.LogModWarnings(this.ModRegistry.GetAll().ToArray(), skippedMods); - - // initialise translations - this.ReloadTranslations(loadedMods); - - // initialise loaded non-content-pack mods - foreach (IModMetadata metadata in loadedMods) - { - // add interceptors - if (metadata.Mod.Helper.Content is ContentHelper helper) - { - // ReSharper disable SuspiciousTypeConversion.Global - if (metadata.Mod is IAssetEditor editor) - helper.ObservableAssetEditors.Add(editor); - if (metadata.Mod is IAssetLoader loader) - helper.ObservableAssetLoaders.Add(loader); - // ReSharper restore SuspiciousTypeConversion.Global - - this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; - } - - // call entry method - try - { - IMod mod = metadata.Mod; - mod.Entry(mod.Helper); - } - catch (Exception ex) - {