From 85f609dc6c2f02d89b9fccaacfe837f8822d6b7c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 May 2017 02:18:58 -0400 Subject: add optional verbose context logging --- src/StardewModdingAPI/Program.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 1e5fcfc3..1913544f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -230,6 +230,7 @@ namespace StardewModdingAPI { // load settings this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); @@ -266,6 +267,8 @@ namespace StardewModdingAPI 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); + if (this.Settings.VerboseLogging) + this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace); // validate XNB integrity if (!this.ValidateContentIntegrity()) -- cgit From 486ac29796586e09540723dcae8070cf3e60285b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:11:39 -0400 Subject: use shared reflection helper --- src/StardewModdingAPI/Program.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 1913544f..aa78ff41 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -40,6 +41,9 @@ namespace StardewModdingAPI /// 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 IReflectionHelper Reflection = new ReflectionHelper(); + /// The underlying game instance. private SGame GameInstance; @@ -141,7 +145,7 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // override game - this.GameInstance = new SGame(this.Monitor); + this.GameInstance = new SGame(this.Monitor, this.Reflection); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -599,7 +603,7 @@ namespace StardewModdingAPI // inject data // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content); + mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; -- cgit From 3d732275874e2f36e13f10813e08b7ed2fc143f8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 01:46:40 -0400 Subject: when a fatal crash happens, keep a copy of the log and notify the player on relaunch --- src/StardewModdingAPI/Program.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index aa78ff41..725ac94f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -154,7 +154,16 @@ namespace StardewModdingAPI this.CancellationTokenSource.Token.WaitHandle.WaitOne(); if (this.IsGameRunning) { - this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit(); + try + { + File.WriteAllText(Constants.FatalCrashMarker, string.Empty); + File.Copy(Constants.DefaultLogPath, Constants.FatalCrashLog, overwrite: true); + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); + } + this.GameInstance.Exit(); } }).Start(); @@ -179,6 +188,17 @@ namespace StardewModdingAPI return; } + // 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 attach this file: {Constants.FatalCrashLog}", 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..."); try -- cgit From 494f9366a8e74f40e085581e8f085a03e3fc9e49 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 22:02:17 -0400 Subject: let mods dispose unmanaged resources when SMAPI is disposing (#282) --- src/StardewModdingAPI/Program.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 725ac94f..a5905805 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -228,14 +228,25 @@ namespace StardewModdingAPI /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { + this.Monitor.Log("Disposing...", LogLevel.Trace); + // skip if already disposed if (this.IsDisposed) return; this.IsDisposed = true; - // dispose mod helpers - foreach (var mod in this.ModRegistry.GetMods()) - (mod.Helper as IDisposable)?.Dispose(); + // dispose mod data + foreach (IMod mod in this.ModRegistry.GetMods()) + { + try + { + (mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {mod.ModManifest.Name} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } // dispose core components this.IsGameRunning = false; -- cgit From 3fa71385e57ba45cde17bf70ad7f027a9734665f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 22:12:02 -0400 Subject: add warning for mods that don't set the UniqueID manifest field --- src/StardewModdingAPI/Program.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a5905805..70b2fbc1 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -501,11 +501,15 @@ namespace StardewModdingAPI this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); continue; } - if (string.IsNullOrEmpty(manifest.EntryDll)) + + // validate manifest + if (string.IsNullOrWhiteSpace(manifest.EntryDll)) { this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } + if (string.IsNullOrWhiteSpace(manifest.UniqueID)) + deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); } catch (Exception ex) { -- cgit From 03876153f4fbb13b8a260c529513a306319f9e05 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 May 2017 22:25:45 -0400 Subject: decouple mod metadata vs assembly loading to enable upcoming mod dependencies (#285) --- src/StardewModdingAPI/Program.cs | 139 +++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 56 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 70b2fbc1..a5bd7788 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -310,7 +310,18 @@ namespace StardewModdingAPI this.Monitor.Log("SMAPI found problems in the game's XNB files which may cause errors or crashes while you're playing. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Warn); // load mods - int modsLoaded = this.LoadMods(); + int modsLoaded; + { + // load mods + JsonHelper jsonHelper = new JsonHelper(); + IList deprecationWarnings = new List(); + ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); + modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); + + // log deprecation warnings together + foreach (Action warning in deprecationWarnings) + warning(); + } if (this.Monitor.IsExiting) { this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); @@ -444,32 +455,27 @@ namespace StardewModdingAPI } } - /// Load and hook up all mods in the mod directory. - /// Returns the number of mods loaded. - private int LoadMods() + /// Find all mods in the given folder. + /// The root mod path to search. + /// The JSON helper with which to read the manifest file. + /// A list to populate with any deprecation warnings. + private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList deprecationWarnings) { - this.Monitor.Log("Loading mods..."); - - // get JSON helper - JsonHelper jsonHelper = new JsonHelper(); - - // get assembly loader - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + this.Monitor.Log("Finding mods..."); + void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); - // load mod assemblies - int modsLoaded = 0; - List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list - foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath)) + // load mod metadata + List mods = new List(); + foreach (string modRootPath in Directory.GetDirectories(rootPath)) { if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn); - return modsLoaded; - } + return new ModMetadata[0]; // exit in progress + + // init metadata + string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); // passthrough empty directories - DirectoryInfo directory = new DirectoryInfo(directoryPath); + DirectoryInfo directory = new DirectoryInfo(modRootPath); while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) directory = directory.GetDirectories().First(); @@ -477,35 +483,34 @@ namespace StardewModdingAPI string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { - this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); + LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); continue; } - string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}"; // read manifest Manifest manifest; try { - // read manifest text + // read manifest file string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { - this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error); + LogSkip(displayName, "its manifest is empty."); continue; } - // deserialise manifest + // parse manifest manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); if (manifest == null) { - this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); + LogSkip(displayName, "its manifest is invalid."); continue; } // validate manifest if (string.IsNullOrWhiteSpace(manifest.EntryDll)) { - this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error); + LogSkip(displayName, "its manifest doesn't set an entry DLL."); continue; } if (string.IsNullOrWhiteSpace(manifest.UniqueID)) @@ -513,11 +518,11 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); continue; } if (!string.IsNullOrWhiteSpace(manifest.Name)) - skippedPrefix = $"Skipped {manifest.Name}"; + displayName = manifest.Name; // validate compatibility ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest); @@ -527,14 +532,13 @@ namespace StardewModdingAPI bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; - string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; if (hasOfficialUrl) - warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; if (hasUnofficialUrl) - warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - this.Monitor.Log(warning, LogLevel.Error); - continue; + LogSkip(displayName, error); } // validate SMAPI version @@ -545,13 +549,13 @@ namespace StardewModdingAPI ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { - this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); + LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } } catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { - this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); + LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); continue; } } @@ -566,39 +570,66 @@ namespace StardewModdingAPI Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { - this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error); + LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); continue; } } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); continue; } } - // validate mod path to simplify errors + // validate DLL path string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { - this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error); + LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); continue; } + // add mod metadata + mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); + } + + return mods.ToArray(); + } + + /// 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. + /// A list to populate with any deprecation warnings. + /// Returns the number of mods successfully loaded. + private int LoadMods(ModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) + { + this.Monitor.Log("Loading mods..."); + void LogSkip(ModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level); + + // load mod assemblies + int modsLoaded = 0; + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + foreach (ModMetadata metadata in mods) + { + IManifest manifest = metadata.Manifest; + string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll); + // preprocess & load mod assembly Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { - this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error); + LogSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version})."); continue; } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } @@ -608,18 +639,18 @@ namespace StardewModdingAPI int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); if (modEntries == 0) { - this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error); + LogSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); continue; } if (modEntries > 1) { - this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error); + LogSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); continue; } } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } @@ -631,16 +662,15 @@ namespace StardewModdingAPI Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { - this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated."); + LogSkip(metadata, "its entry class couldn't be instantiated."); continue; } // inject data - // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content, this.Reflection); + mod.Helper = new ModHelper(manifest, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); - mod.PathOnDisk = directory.FullName; + mod.PathOnDisk = metadata.DirectoryPath; // track mod this.ModRegistry.Add(mod); @@ -649,11 +679,11 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); } } - // initialise mods + // initialise loaded mods foreach (IMod mod in this.ModRegistry.GetMods()) { try @@ -674,9 +704,6 @@ namespace StardewModdingAPI // print result this.Monitor.Log($"Loaded {modsLoaded} mods."); - foreach (Action warning in deprecationWarnings) - warning(); - return modsLoaded; } -- cgit From bb165f2079e33d02c0e673db73ac5b336272a3fa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 May 2017 23:21:02 -0400 Subject: organise a few framework classes --- src/StardewModdingAPI/Program.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a5bd7788..b8d70ad7 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewValley; -- cgit From e84028f22bf3fc81682ff6fb3a59293f23c030f4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 12 May 2017 00:01:39 -0400 Subject: fix SMAPI raising a deprecation warning for its own use of an event --- src/StardewModdingAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b8d70ad7..75bdba0f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -176,7 +176,7 @@ namespace StardewModdingAPI this.GameInstance.Exiting += (sender, e) => this.Dispose(); this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); - GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); + GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); // set window titles this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion}"; -- cgit From 3a02402367fb13787d7ee18315273db9f299b7d9 Mon Sep 17 00:00:00 2001 From: Luke Wale Date: Sat, 13 May 2017 19:25:13 +0800 Subject: Added basic topological sort for mod dependencies (#285) --- src/StardewModdingAPI/Program.cs | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 75bdba0f..b9ddb527 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -317,6 +317,7 @@ namespace StardewModdingAPI JsonHelper jsonHelper = new JsonHelper(); IList deprecationWarnings = new List(); ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); + mods = SortByDependencies(mods); modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); // log deprecation warnings together @@ -337,6 +338,88 @@ namespace StardewModdingAPI new Thread(this.RunConsoleLoop).Start(); } + private ModMetadata[] SortByDependencies(ModMetadata[] mods) + { + var unsortedMods = mods.ToList(); + var sortedMods = new Stack(); + var visitedMods = new bool[unsortedMods.Count]; + var currentChain = new List(); + bool success = true; + + for (int modIndex = 0; modIndex < unsortedMods.Count; modIndex++) + { + if (visitedMods[modIndex] == false) + { + success = SortByDependencies(modIndex, visitedMods, sortedMods, currentChain, unsortedMods); + } + if (!success) break; + } + + if (!success) + { + // Failed to sort list, return no mods. + this.Monitor.Log("No mods will be loaded.", LogLevel.Error); + return new ModMetadata[0]; + } + + return sortedMods.Reverse().ToArray(); + } + + private bool SortByDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) + { + visitedMods[modIndex] = true; + var mod = unsortedMods[modIndex]; + + string missingMods = string.Empty; + foreach (var m in mod.Manifest.Dependencies) + { + if (!unsortedMods.Any(x => x.Manifest.UniqueID.Equals(m.UniqueID))) + { + missingMods += $",{m.UniqueID}"; + } + } + if (!string.IsNullOrEmpty(missingMods)) + { + this.Monitor.Log($"Mod {mod.Manifest.UniqueID} is missing dependencies; {missingMods.TrimStart(',')}", LogLevel.Error); + return false; + } + + var modsToLoadFirst = unsortedMods.Where(x => + mod.Manifest.Dependencies.Any(y => y.UniqueID == x.Manifest.UniqueID) + ).ToList(); + + var circularReferenceMod = currentChain.FirstOrDefault(x => modsToLoadFirst.Contains(x)); + if (circularReferenceMod != null) + { + this.Monitor.Log($"Circular reference found when loading Mod dependencies.", LogLevel.Error); + string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; + for (int i = currentChain.Count - 1; i >= 0; i--) + { + chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; + if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; + } + this.Monitor.Log(chain, LogLevel.Error); + return false; + } + + currentChain.Add(mod); + + bool success = true; + foreach (var requiredMod in modsToLoadFirst) + { + int index = unsortedMods.IndexOf(requiredMod); + if (!visitedMods[index]) + { + success = SortByDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + } + if (!success) break; + } + + sortedMods.Push(mod); + currentChain.Remove(mod); + return success; + } + /// Run a loop handling console input. [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] private void RunConsoleLoop() -- cgit From a3729c36f55e36f7159a2c88a939d17f9f486e61 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 16:58:17 -0400 Subject: refactor mod dependency logic a bit (#285) --- src/StardewModdingAPI/Program.cs | 80 ++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 32 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b9ddb527..3ec9e28b 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -317,7 +317,7 @@ namespace StardewModdingAPI JsonHelper jsonHelper = new JsonHelper(); IList deprecationWarnings = new List(); ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); - mods = SortByDependencies(mods); + mods = this.HandleModDependencies(mods); modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); // log deprecation warnings together @@ -338,7 +338,9 @@ namespace StardewModdingAPI new Thread(this.RunConsoleLoop).Start(); } - private ModMetadata[] SortByDependencies(ModMetadata[] mods) + /// Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. + /// The mods to process. + private ModMetadata[] HandleModDependencies(ModMetadata[] mods) { var unsortedMods = mods.ToList(); var sortedMods = new Stack(); @@ -346,13 +348,11 @@ namespace StardewModdingAPI var currentChain = new List(); bool success = true; - for (int modIndex = 0; modIndex < unsortedMods.Count; modIndex++) + for (int index = 0; index < unsortedMods.Count; index++) { - if (visitedMods[modIndex] == false) - { - success = SortByDependencies(modIndex, visitedMods, sortedMods, currentChain, unsortedMods); - } - if (!success) break; + success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + if (!success) + break; } if (!success) @@ -365,33 +365,50 @@ namespace StardewModdingAPI return sortedMods.Reverse().ToArray(); } - private bool SortByDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) + /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. + /// The index of the mod being processed in the . + /// The mods which have been processed. + /// The list in which to save mods sorted by dependency order. + /// The current change of mod dependencies. + /// The mods remaining to sort. + /// Returns whether the mod can be loaded. + private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) { + // visit mod + if (visitedMods[modIndex]) + return true; // already sorted + ModMetadata mod = unsortedMods[modIndex]; visitedMods[modIndex] = true; - var mod = unsortedMods[modIndex]; - string missingMods = string.Empty; - foreach (var m in mod.Manifest.Dependencies) + // validate required dependencies are present { - if (!unsortedMods.Any(x => x.Manifest.UniqueID.Equals(m.UniqueID))) + string missingMods = null; + foreach (IManifestDependency dependency in mod.Manifest.Dependencies) { - missingMods += $",{m.UniqueID}"; + if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) + missingMods += $"{dependency.UniqueID}, "; + } + if (missingMods != null) + { + this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); + return false; } } - if (!string.IsNullOrEmpty(missingMods)) - { - this.Monitor.Log($"Mod {mod.Manifest.UniqueID} is missing dependencies; {missingMods.TrimStart(',')}", LogLevel.Error); - return false; - } - - var modsToLoadFirst = unsortedMods.Where(x => - mod.Manifest.Dependencies.Any(y => y.UniqueID == x.Manifest.UniqueID) - ).ToList(); - var circularReferenceMod = currentChain.FirstOrDefault(x => modsToLoadFirst.Contains(x)); + // get mods which should be loaded before this one + ModMetadata[] modsToLoadFirst = + ( + from unsorted in unsortedMods + where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) + select unsorted + ) + .ToArray(); + + // detect circular references + ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); if (circularReferenceMod != null) { - this.Monitor.Log($"Circular reference found when loading Mod dependencies.", LogLevel.Error); + this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; for (int i = currentChain.Count - 1; i >= 0; i--) { @@ -401,20 +418,19 @@ namespace StardewModdingAPI this.Monitor.Log(chain, LogLevel.Error); return false; } - currentChain.Add(mod); + // recursively sort dependencies bool success = true; - foreach (var requiredMod in modsToLoadFirst) + foreach (ModMetadata requiredMod in modsToLoadFirst) { int index = unsortedMods.IndexOf(requiredMod); - if (!visitedMods[index]) - { - success = SortByDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); - } - if (!success) break; + success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + if (!success) + break; } + // mark mod sorted sortedMods.Push(mod); currentChain.Remove(mod); return success; -- cgit From c932c5313705d0b9b0ed566c22d6a4935b69897c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 17:03:26 -0400 Subject: fix error when processing mods that have no dependencies (#285) --- src/StardewModdingAPI/Program.cs | 83 +++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 39 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3ec9e28b..a86a9540 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -342,6 +342,7 @@ namespace StardewModdingAPI /// The mods to process. private ModMetadata[] HandleModDependencies(ModMetadata[] mods) { + this.Monitor.Log("Checking mod dependencies..."); var unsortedMods = mods.ToList(); var sortedMods = new Stack(); var visitedMods = new bool[unsortedMods.Count]; @@ -380,54 +381,58 @@ namespace StardewModdingAPI ModMetadata mod = unsortedMods[modIndex]; visitedMods[modIndex] = true; - // validate required dependencies are present + // process dependencies + bool success = true; + if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any()) { - string missingMods = null; - foreach (IManifestDependency dependency in mod.Manifest.Dependencies) + // validate required dependencies are present { - if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) - missingMods += $"{dependency.UniqueID}, "; + string missingMods = null; + foreach (IManifestDependency dependency in mod.Manifest.Dependencies) + { + if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) + missingMods += $"{dependency.UniqueID}, "; + } + if (missingMods != null) + { + this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); + return false; + } } - if (missingMods != null) + + // get mods which should be loaded before this one + ModMetadata[] modsToLoadFirst = + ( + from unsorted in unsortedMods + where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) + select unsorted + ) + .ToArray(); + + // detect circular references + ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); + if (circularReferenceMod != null) { - this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); + this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); + string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; + for (int i = currentChain.Count - 1; i >= 0; i--) + { + chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; + if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; + } + this.Monitor.Log(chain, LogLevel.Error); return false; } - } + currentChain.Add(mod); - // get mods which should be loaded before this one - ModMetadata[] modsToLoadFirst = - ( - from unsorted in unsortedMods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) - select unsorted - ) - .ToArray(); - - // detect circular references - ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); - if (circularReferenceMod != null) - { - this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); - string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; - for (int i = currentChain.Count - 1; i >= 0; i--) + // recursively sort dependencies + foreach (ModMetadata requiredMod in modsToLoadFirst) { - chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; - if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; + int index = unsortedMods.IndexOf(requiredMod); + success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + if (!success) + break; } - this.Monitor.Log(chain, LogLevel.Error); - return false; - } - currentChain.Add(mod); - - // recursively sort dependencies - bool success = true; - foreach (ModMetadata requiredMod in modsToLoadFirst) - { - int index = unsortedMods.IndexOf(requiredMod); - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); - if (!success) - break; } // mark mod sorted -- cgit From 66d2b5746ab063b89ca42525a78e217e71d00858 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 17:24:41 -0400 Subject: move mod metadata resolution into its own class (#285) --- src/StardewModdingAPI/Program.cs | 250 +-------------------------------------- 1 file changed, 3 insertions(+), 247 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a86a9540..7b421895 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -269,7 +269,7 @@ namespace StardewModdingAPI this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components - this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); + this.ModRegistry = new ModRegistry(); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); @@ -316,8 +316,8 @@ namespace StardewModdingAPI // load mods JsonHelper jsonHelper = new JsonHelper(); IList deprecationWarnings = new List(); - ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); - mods = this.HandleModDependencies(mods); + ModMetadata[] mods = new ModResolver(this.Monitor, this.DeprecationManager, this.Settings.ModCompatibility) + .FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); // log deprecation warnings together @@ -338,109 +338,6 @@ namespace StardewModdingAPI new Thread(this.RunConsoleLoop).Start(); } - /// Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. - /// The mods to process. - private ModMetadata[] HandleModDependencies(ModMetadata[] mods) - { - this.Monitor.Log("Checking mod dependencies..."); - var unsortedMods = mods.ToList(); - var sortedMods = new Stack(); - var visitedMods = new bool[unsortedMods.Count]; - var currentChain = new List(); - bool success = true; - - for (int index = 0; index < unsortedMods.Count; index++) - { - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); - if (!success) - break; - } - - if (!success) - { - // Failed to sort list, return no mods. - this.Monitor.Log("No mods will be loaded.", LogLevel.Error); - return new ModMetadata[0]; - } - - return sortedMods.Reverse().ToArray(); - } - - /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. - /// The index of the mod being processed in the . - /// The mods which have been processed. - /// The list in which to save mods sorted by dependency order. - /// The current change of mod dependencies. - /// The mods remaining to sort. - /// Returns whether the mod can be loaded. - private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) - { - // visit mod - if (visitedMods[modIndex]) - return true; // already sorted - ModMetadata mod = unsortedMods[modIndex]; - visitedMods[modIndex] = true; - - // process dependencies - bool success = true; - if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any()) - { - // validate required dependencies are present - { - string missingMods = null; - foreach (IManifestDependency dependency in mod.Manifest.Dependencies) - { - if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) - missingMods += $"{dependency.UniqueID}, "; - } - if (missingMods != null) - { - this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); - return false; - } - } - - // get mods which should be loaded before this one - ModMetadata[] modsToLoadFirst = - ( - from unsorted in unsortedMods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) - select unsorted - ) - .ToArray(); - - // detect circular references - ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); - if (circularReferenceMod != null) - { - this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); - string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; - for (int i = currentChain.Count - 1; i >= 0; i--) - { - chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; - if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; - } - this.Monitor.Log(chain, LogLevel.Error); - return false; - } - currentChain.Add(mod); - - // recursively sort dependencies - foreach (ModMetadata requiredMod in modsToLoadFirst) - { - int index = unsortedMods.IndexOf(requiredMod); - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); - if (!success) - break; - } - } - - // mark mod sorted - sortedMods.Push(mod); - currentChain.Remove(mod); - return success; - } - /// Run a loop handling console input. [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] private void RunConsoleLoop() @@ -560,147 +457,6 @@ namespace StardewModdingAPI } } - /// Find all mods in the given folder. - /// The root mod path to search. - /// The JSON helper with which to read the manifest file. - /// A list to populate with any deprecation warnings. - private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList deprecationWarnings) - { - this.Monitor.Log("Finding mods..."); - void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); - - // load mod metadata - List mods = new List(); - foreach (string modRootPath in Directory.GetDirectories(rootPath)) - { - if (this.Monitor.IsExiting) - return new ModMetadata[0]; // exit in progress - - // init metadata - string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); - - // passthrough empty directories - DirectoryInfo directory = new DirectoryInfo(modRootPath); - while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) - directory = directory.GetDirectories().First(); - - // get manifest path - string manifestPath = Path.Combine(directory.FullName, "manifest.json"); - if (!File.Exists(manifestPath)) - { - LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); - continue; - } - - // read manifest - Manifest manifest; - try - { - // read manifest file - string json = File.ReadAllText(manifestPath); - if (string.IsNullOrEmpty(json)) - { - LogSkip(displayName, "its manifest is empty."); - continue; - } - - // parse manifest - manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); - if (manifest == null) - { - LogSkip(displayName, "its manifest is invalid."); - continue; - } - - // validate manifest - if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - { - LogSkip(displayName, "its manifest doesn't set an entry DLL."); - continue; - } - if (string.IsNullOrWhiteSpace(manifest.UniqueID)) - deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); - } - catch (Exception ex) - { - LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); - continue; - } - if (!string.IsNullOrWhiteSpace(manifest.Name)) - displayName = manifest.Name; - - // validate compatibility - ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest); - if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) - { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); - - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; - if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; - if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - - LogSkip(displayName, error); - } - - // validate SMAPI version - if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) - { - try - { - ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); - if (minVersion.IsNewerThan(Constants.ApiVersion)) - { - LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); - continue; - } - } - catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) - { - LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); - continue; - } - } - - // create per-save directory - if (manifest.PerSaveConfigs) - { - 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)) - { - LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); - continue; - } - } - catch (Exception ex) - { - LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); - continue; - } - } - - // validate DLL path - string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); - continue; - } - - // add mod metadata - mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); - } - - return mods.ToArray(); - } - /// Load and hook up the given mods. /// The mods to load. /// The JSON helper with which to read mods' JSON files. -- cgit From 63edebaef1019ce103f5a86d55e1d1c4eb8d371c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 18:20:09 -0400 Subject: decouple mod metadata resolution from main SMAPI logic (#285) This makes the logic more self-contained for eventual unit testing, and makes failed mods available during dependency resolution so we can make errors more relevant. --- src/StardewModdingAPI/Program.cs | 51 +++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 7b421895..c8840538 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -313,14 +313,45 @@ namespace StardewModdingAPI // load mods int modsLoaded; { - // load mods + // get mod metadata (in dependency order) + this.Monitor.Log("Loading mod me