From 61d6ec12daee843f758e5f828a713a72a767a94b Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 18 Oct 2022 20:03:28 -0500 Subject: add detailed manifest validation errors at build time --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 98 +++------------------------ 1 file changed, 10 insertions(+), 88 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fe56f4d2..352c22cc 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -8,7 +8,6 @@ using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Toolkit.Serialization.Models; -using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities.PathLookups; namespace StardewModdingAPI.Framework.ModLoading @@ -126,100 +125,23 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL / content pack fields + // check for dll if it's supposed to have one + if (!string.IsNullOrEmpty(mod.Manifest.EntryDll) && validateFilesExist) { - bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); - bool isContentPack = mod.Manifest.ContentPackFor != null; - - // validate field presence - if (!hasDll && !isContentPack) + IFileLookup pathLookup = getFileLookup(mod.DirectoryPath); + FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!); + if (!file.Exists) { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); - continue; - } - if (hasDll && isContentPack) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); - continue; - } - - // validate DLL - if (hasDll) - { - // invalid filename format - if (mod.Manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } - - // file doesn't exist - if (validateFilesExist) - { - IFileLookup pathLookup = getFileLookup(mod.DirectoryPath); - FileInfo file = pathLookup.GetFile(mod.Manifest.EntryDll!); - if (!file.Exists) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; - } - } - } - - // validate content pack - else - { - // invalid content pack ID - if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor!.UniqueID)) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); - continue; - } - } - } - - // validate required fields - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } } - // validate ID format - if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); - - // validate dependencies - foreach (IManifestDependency? dependency in mod.Manifest.Dependencies) + // validate manifest + if (mod.Manifest is Manifest manifest && !manifest.TryValidate(out string manifestError)) { - // null dependency - if (dependency == null) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a null entry under {nameof(IManifest.Dependencies)}."); - continue; - } - - // missing ID - if (string.IsNullOrWhiteSpace(dependency.UniqueID)) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."); - continue; - } - - // invalid ID - if (!PathUtilities.IsSlug(dependency.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}"); + continue; } } -- cgit From bb2fde18292352471501887013ca2b7f60a9dc25 Mon Sep 17 00:00:00 2001 From: Michał Dolaś Date: Wed, 9 Nov 2022 17:25:25 +0100 Subject: Added ModsToLoadFirst/Last to SMAPI config, along with the implementation --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 22 +++++++++++++++++----- src/SMAPI/Framework/Models/SConfig.cs | 12 +++++++++++- src/SMAPI/Framework/SCore.cs | 10 +++++++++- src/SMAPI/SMAPI.config.json | 12 +++++++++++- 4 files changed, 48 insertions(+), 8 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fe56f4d2..b90f9ba5 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -245,7 +245,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. /// Handles access to SMAPI's internal mod metadata list. - public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase) + /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. + public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadFirst, IReadOnlyList modIdsToLoadLast, ModDatabase modDatabase) { // initialize metadata mods = mods.ToArray(); @@ -260,8 +262,18 @@ namespace StardewModdingAPI.Framework.ModLoading } // sort mods - foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List()); + IModMetadata[] allMods = mods.ToArray(); + IModMetadata[] modsToLoadFirst = allMods.Where(m => modIdsToLoadFirst.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadLast = allMods.Where(m => modIdsToLoadLast.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadFirst.Contains(m) && !modsToLoadLast.Contains(m)).ToArray(); + + List orderSortedMods = new(); + orderSortedMods.AddRange(modsToLoadFirst); + orderSortedMods.AddRange(modsToLoadAsUsual); + orderSortedMods.AddRange(modsToLoadLast); + + foreach (IModMetadata mod in orderSortedMods) + this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List()); return sortedMods.Reverse(); } @@ -278,7 +290,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The list in which to save mods sorted by dependency order. /// The current change of mod dependencies. /// Returns the mod dependency status. - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) + private ModDependencyStatus ProcessDependencies(IReadOnlyList mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) { // check if already visited switch (states[mod]) @@ -409,7 +421,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// Get the dependencies declared in a manifest. /// The mod manifest. /// The loaded mods. - private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + private IEnumerable GetDependenciesFrom(IManifest manifest, IReadOnlyList loadedMods) { IModMetadata? FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id)); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index bceb0940..ddd721d5 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -82,6 +82,12 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public HashSet SuppressUpdateChecks { get; set; } + /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. + public List ModsToLoadFirst { get; set; } + + /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. + public List ModsToLoadLast { get; set; } + /******** ** Public methods @@ -100,7 +106,9 @@ namespace StardewModdingAPI.Framework.Models /// The colors to use for text written to the SMAPI console. /// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks) + /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadFirst, string[]? modsToLoadLast) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -115,6 +123,8 @@ namespace StardewModdingAPI.Framework.Models this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + this.ModsToLoadFirst = new List(modsToLoadFirst ?? Array.Empty()); + this.ModsToLoadLast = new List(modsToLoadLast ?? Array.Empty()); } /// Override the value of . diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 40979b09..7bd60490 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,9 +423,17 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); mods = mods.Where(p => !p.IsIgnored).ToArray(); + // warn about mods that should load first or last which are not found at all + foreach (string modId in this.Settings.ModsToLoadFirst) + if (!mods.Any(m => m.Manifest.UniqueID == modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load first, but it could not be found.", LogLevel.Warn); + foreach (string modId in this.Settings.ModsToLoadLast) + if (!mods.Any(m => m.Manifest.UniqueID == modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load last, but it could not be found.", LogLevel.Warn); + // load mods resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); - mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); + mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadFirst, this.Settings.ModsToLoadLast, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // check for software likely to cause issues diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 635e3add..1a342df2 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -141,5 +141,15 @@ copy all the settings, or you may cause bugs due to overridden changes in future "SMAPI.ConsoleCommands", "SMAPI.ErrorHandler", "SMAPI.SaveBackup" - ] + ], + + /** + * The mod IDs SMAPI should try to load first, before any other mods not included in this list. + */ + "ModsToLoadFirst": [], + + /** + * The mod IDs SMAPI should try to load last, after all other mods not included in this list. + */ + "ModsToLoadLast": [] } -- cgit From 42b4b6b6a4ae1bb59182857b383539b24b063215 Mon Sep 17 00:00:00 2001 From: Michał Dolaś Date: Wed, 9 Nov 2022 19:50:32 +0100 Subject: Renamed first/last to early/late; ignoring mods declared as both and warning about those --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 ++++++++-------- src/SMAPI/Framework/Models/SConfig.cs | 18 +++++++++--------- src/SMAPI/Framework/SCore.cs | 15 +++++++++------ src/SMAPI/SMAPI.config.json | 8 ++++---- 4 files changed, 30 insertions(+), 27 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b90f9ba5..f9ba73c4 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -245,9 +245,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. /// Handles access to SMAPI's internal mod metadata list. - /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. - /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. - public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadFirst, IReadOnlyList modIdsToLoadLast, ModDatabase modDatabase) + /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadEarly, IReadOnlyList modIdsToLoadLate, ModDatabase modDatabase) { // initialize metadata mods = mods.ToArray(); @@ -263,14 +263,14 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods IModMetadata[] allMods = mods.ToArray(); - IModMetadata[] modsToLoadFirst = allMods.Where(m => modIdsToLoadFirst.Contains(m.Manifest.UniqueID)).ToArray(); - IModMetadata[] modsToLoadLast = allMods.Where(m => modIdsToLoadLast.Contains(m.Manifest.UniqueID)).ToArray(); - IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadFirst.Contains(m) && !modsToLoadLast.Contains(m)).ToArray(); + IModMetadata[] modsToLoadEarly = allMods.Where(m => modIdsToLoadEarly.Contains(m.Manifest.UniqueID) && !modIdsToLoadLate.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadLate = allMods.Where(m => modIdsToLoadLate.Contains(m.Manifest.UniqueID) && !modIdsToLoadEarly.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray(); List orderSortedMods = new(); - orderSortedMods.AddRange(modsToLoadFirst); + orderSortedMods.AddRange(modsToLoadEarly); orderSortedMods.AddRange(modsToLoadAsUsual); - orderSortedMods.AddRange(modsToLoadLast); + orderSortedMods.AddRange(modsToLoadLate); foreach (IModMetadata mod in orderSortedMods) this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List()); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index ddd721d5..40d3450f 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public HashSet SuppressUpdateChecks { get; set; } - /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. - public List ModsToLoadFirst { get; set; } + /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. + public List ModsToLoadEarly { get; set; } - /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. - public List ModsToLoadLast { get; set; } + /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + public List ModsToLoadLate { get; set; } /******** @@ -106,9 +106,9 @@ namespace StardewModdingAPI.Framework.Models /// The colors to use for text written to the SMAPI console. /// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - /// The mod IDs SMAPI should try to load first, before any other mods not included in this list. - /// The mod IDs SMAPI should try to load last, after all other mods not included in this list. - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadFirst, string[]? modsToLoadLast) + /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. + /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -123,8 +123,8 @@ namespace StardewModdingAPI.Framework.Models this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); - this.ModsToLoadFirst = new List(modsToLoadFirst ?? Array.Empty()); - this.ModsToLoadLast = new List(modsToLoadLast ?? Array.Empty()); + this.ModsToLoadEarly = new List(modsToLoadEarly ?? Array.Empty()); + this.ModsToLoadLate = new List(modsToLoadLate ?? Array.Empty()); } /// Override the value of . diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 7bd60490..9e91924e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,17 +423,20 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); mods = mods.Where(p => !p.IsIgnored).ToArray(); - // warn about mods that should load first or last which are not found at all - foreach (string modId in this.Settings.ModsToLoadFirst) + // warn about mods that should load early or late which are not found at all, or both + foreach (string modId in this.Settings.ModsToLoadEarly) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load first, but it could not be found.", LogLevel.Warn); - foreach (string modId in this.Settings.ModsToLoadLast) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found.", LogLevel.Warn); + foreach (string modId in this.Settings.ModsToLoadLate) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load last, but it could not be found.", LogLevel.Warn); + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found.", LogLevel.Warn); + foreach (string modId in this.Settings.ModsToLoadEarly) + if (this.Settings.ModsToLoadLate.Contains(modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn); // load mods resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); - mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadFirst, this.Settings.ModsToLoadLast, modDatabase).ToArray(); + mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // check for software likely to cause issues diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 1a342df2..68645d24 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -144,12 +144,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future ], /** - * The mod IDs SMAPI should try to load first, before any other mods not included in this list. + * The mod IDs SMAPI should try to load early, before any other mods not included in this list. */ - "ModsToLoadFirst": [], + "ModsToLoadEarly": [], /** - * The mod IDs SMAPI should try to load last, after all other mods not included in this list. + * The mod IDs SMAPI should try to load late, after all other mods not included in this list. */ - "ModsToLoadLast": [] + "ModsToLoadLate": [] } -- cgit From 9fd8c35b462bc19efb520da21cda66f83559a66e Mon Sep 17 00:00:00 2001 From: Michał Dolaś Date: Wed, 9 Nov 2022 20:26:50 +0100 Subject: Actually taking order into consideration --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 14 ++++++++++++-- src/SMAPI/Framework/SCore.cs | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index f9ba73c4..8ef5e4a8 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -263,8 +263,18 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods IModMetadata[] allMods = mods.ToArray(); - IModMetadata[] modsToLoadEarly = allMods.Where(m => modIdsToLoadEarly.Contains(m.Manifest.UniqueID) && !modIdsToLoadLate.Contains(m.Manifest.UniqueID)).ToArray(); - IModMetadata[] modsToLoadLate = allMods.Where(m => modIdsToLoadLate.Contains(m.Manifest.UniqueID) && !modIdsToLoadEarly.Contains(m.Manifest.UniqueID)).ToArray(); + IModMetadata[] modsToLoadEarly = modIdsToLoadEarly + .Where(modId => !modIdsToLoadLate.Contains(modId)) + .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId)) + .Where(m => m != null) + .Select(m => m!) + .ToArray(); + IModMetadata[] modsToLoadLate = modIdsToLoadLate + .Where(modId => !modIdsToLoadEarly.Contains(modId)) + .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId)) + .Where(m => m != null) + .Select(m => m!) + .ToArray(); IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray(); List orderSortedMods = new(); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9e91924e..4d1eb959 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -426,10 +426,10 @@ namespace StardewModdingAPI.Framework // warn about mods that should load early or late which are not found at all, or both foreach (string modId in this.Settings.ModsToLoadEarly) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found.", LogLevel.Warn); + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found or was skipped.", LogLevel.Warn); foreach (string modId in this.Settings.ModsToLoadLate) if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found.", LogLevel.Warn); + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn); foreach (string modId in this.Settings.ModsToLoadEarly) if (this.Settings.ModsToLoadLate.Contains(modId)) this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn); -- cgit From 346fddda670704c1458e42104ee7405fc1de7ccc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 Nov 2022 23:27:38 -0500 Subject: move validation logic out of Manifest model This avoids tightly coupling higher logic to the implementation class, since we can validate the interface. --- src/SMAPI.ModBuildConfig/DeployModTask.cs | 6 +- src/SMAPI.Toolkit/Framework/ManifestValidator.cs | 101 +++++++++++++++++++++ src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 92 ------------------- src/SMAPI/Framework/ModLoading/ModResolver.cs | 3 +- 4 files changed, 107 insertions(+), 95 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/ManifestValidator.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 357e02b5..70761a2f 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -9,6 +9,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Newtonsoft.Json; using StardewModdingAPI.ModBuildConfig.Framework; +using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; @@ -91,7 +92,8 @@ namespace StardewModdingAPI.ModBuildConfig try { new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest); - } catch (JsonReaderException ex) + } + catch (JsonReaderException ex) { // log the inner exception, otherwise the message will be generic Exception exToShow = ex.InnerException ?? ex; @@ -100,7 +102,7 @@ namespace StardewModdingAPI.ModBuildConfig } // validate the manifest's fields - if (!manifest.TryValidate(out string error)) + if (!ManifestValidator.TryValidate(manifest, out string error)) { this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}"); return false; diff --git a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs new file mode 100644 index 00000000..62cfd8e9 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework +{ + /// Validates manifest fields. + public static class ManifestValidator + { + /// Try to validate a manifest's fields. Fails if any invalid field is found. + /// The manifest to validate. + /// The error message to display to the user. + /// Returns whether the manifest was validated successfully. + public static bool TryValidate(IManifest manifest, out string error) + { + // validate DLL / content pack fields + bool hasDll = !string.IsNullOrWhiteSpace(manifest.EntryDll); + bool isContentPack = manifest.ContentPackFor != null; + + // validate field presence + if (!hasDll && !isContentPack) + { + error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; + return false; + } + if (hasDll && isContentPack) + { + error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."; + return false; + } + + // validate DLL filename format + if (hasDll && manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + error = $"manifest has invalid filename '{manifest.EntryDll}' for the EntryDLL field."; + return false; + } + + // validate content pack ID + else if (isContentPack && string.IsNullOrWhiteSpace(manifest.ContentPackFor!.UniqueID)) + { + error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; + return false; + } + + // validate required fields + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(manifest.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (manifest.Version == null || manifest.Version.ToString() == "0.0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(manifest.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + { + error = $"manifest is missing required fields ({string.Join(", ", missingFields)})."; + return false; + } + } + + // validate ID format + if (!PathUtilities.IsSlug(manifest.UniqueID)) + { + error = "manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; + return false; + } + + // validate dependencies + foreach (IManifestDependency? dependency in manifest.Dependencies) + { + // null dependency + if (dependency == null) + { + error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}."; + return false; + } + + // missing ID + if (string.IsNullOrWhiteSpace(dependency.UniqueID)) + { + error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."; + return false; + } + + // invalid ID + if (!PathUtilities.IsSlug(dependency.UniqueID)) + { + error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; + return false; + } + } + + error = ""; + return true; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 1607cf3e..8a449f0a 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Text; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; -using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Serialization.Models { @@ -106,95 +103,6 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models this.UpdateKeys = updateKeys ?? Array.Empty(); } - /// Try to validate a manifest's fields. Fails if any invalid field is found. - /// The error message to display to the user. - /// Returns whether the manifest was validated successfully. - public bool TryValidate(out string error) - { - // validate DLL / content pack fields - bool hasDll = !string.IsNullOrWhiteSpace(this.EntryDll); - bool isContentPack = this.ContentPackFor != null; - - // validate field presence - if (!hasDll && !isContentPack) - { - error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; - return false; - } - if (hasDll && isContentPack) - { - error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."; - return false; - } - - // validate DLL filename format - if (hasDll && this.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - error = $"manifest has invalid filename '{this.EntryDll}' for the EntryDLL field."; - return false; - } - - // validate content pack ID - else if (isContentPack && string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID)) - { - error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; - return false; - } - - // validate required fields - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(this.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (this.Version == null || this.Version.ToString() == "0.0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(this.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - { - error = $"manifest is missing required fields ({string.Join(", ", missingFields)})."; - return false; - } - } - - // validate ID format - if (!PathUtilities.IsSlug(this.UniqueID)) - { - error = "manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; - return false; - } - - // validate dependencies - foreach (IManifestDependency? dependency in this.Dependencies) - { - // null dependency - if (dependency == null) - { - error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}."; - return false; - } - - // missing ID - if (string.IsNullOrWhiteSpace(dependency.UniqueID)) - { - error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."; - return false; - } - - // invalid ID - if (!PathUtilities.IsSlug(dependency.UniqueID)) - { - error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; - return false; - } - } - - error = ""; - return true; - } - /// Override the update keys loaded from the mod info. /// The new update keys to set. internal void OverrideUpdateKeys(params string[] updateKeys) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 352c22cc..0b4fe3e9 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -138,7 +139,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate manifest - if (mod.Manifest is Manifest manifest && !manifest.TryValidate(out string manifestError)) + if (!ManifestValidator.TryValidate(mod.Manifest, out string manifestError)) { mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}"); continue; -- cgit From 867afdd96ff8896dc81fdab204cf045713d32d91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 Nov 2022 23:27:38 -0500 Subject: tweak new code --- src/SMAPI.ModBuildConfig/DeployModTask.cs | 27 +++++------ src/SMAPI.Toolkit/Framework/ManifestValidator.cs | 57 +++++++++++++----------- src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 +++---- 3 files changed, 51 insertions(+), 49 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 1581b282..3508a6db 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -85,33 +85,30 @@ namespace StardewModdingAPI.ModBuildConfig return true; // validate the manifest file - Manifest manifest; + IManifest manifest; { - // check if manifest file exists - FileInfo manifestFile = new(Path.Combine(this.ProjectDir, "manifest.json")); - if (!manifestFile.Exists) - { - this.Log.LogError("[mod build package] The mod does not have a manifest.json file."); - return false; - } - - // check if the json is valid try { - new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest); + string manifestPath = Path.Combine(this.ProjectDir, "manifest.json"); + if (!new JsonHelper().ReadJsonFileIfExists(manifestPath, out Manifest rawManifest)) + { + this.Log.LogError("[mod build package] The mod's manifest.json file doesn't exist."); + return false; + } + manifest = rawManifest; } catch (JsonReaderException ex) { // log the inner exception, otherwise the message will be generic Exception exToShow = ex.InnerException ?? ex; - this.Log.LogError($"[mod build package] Failed to parse manifest.json: {exToShow.Message}"); + this.Log.LogError($"[mod build package] The mod's manifest.json file isn't valid JSON: {exToShow.Message}"); return false; } - // validate the manifest's fields - if (!ManifestValidator.TryValidate(manifest, out string error)) + // validate manifest fields + if (!ManifestValidator.TryValidateFields(manifest, out string error)) { - this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}"); + this.Log.LogError($"[mod build package] The mod's manifest.json file is invalid: {error}"); return false; } } diff --git a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs index 62cfd8e9..461dc325 100644 --- a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs +++ b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using StardewModdingAPI.Toolkit.Utilities; @@ -8,40 +9,47 @@ namespace StardewModdingAPI.Toolkit.Framework /// Validates manifest fields. public static class ManifestValidator { - /// Try to validate a manifest's fields. Fails if any invalid field is found. + /// Validate a manifest's fields. /// The manifest to validate. - /// The error message to display to the user. - /// Returns whether the manifest was validated successfully. - public static bool TryValidate(IManifest manifest, out string error) + /// The error message indicating why validation failed, if applicable. + /// Returns whether all manifest fields validated successfully. + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method that ensures those annotations are respected.")] + public static bool TryValidateFields(IManifest manifest, out string error) { - // validate DLL / content pack fields + // + // Note: SMAPI assumes that it can grammatically append the returned sentence in the + // form "failed loading because its ". Any errors returned should be valid + // in that format, unless the SMAPI call is adjusted accordingly. + // + bool hasDll = !string.IsNullOrWhiteSpace(manifest.EntryDll); bool isContentPack = manifest.ContentPackFor != null; - // validate field presence - if (!hasDll && !isContentPack) - { - error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; - return false; - } - if (hasDll && isContentPack) + // validate use of EntryDll vs ContentPackFor fields + if (hasDll == isContentPack) { - error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."; + error = hasDll + ? $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive." + : $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; return false; } - // validate DLL filename format - if (hasDll && manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) + // validate EntryDll/ContentPackFor format + if (hasDll) { - error = $"manifest has invalid filename '{manifest.EntryDll}' for the EntryDLL field."; - return false; + if (manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + error = $"manifest has invalid filename '{manifest.EntryDll}' for the {nameof(IManifest.EntryDll)} field."; + return false; + } } - - // validate content pack ID - else if (isContentPack && string.IsNullOrWhiteSpace(manifest.ContentPackFor!.UniqueID)) + else { - error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; - return false; + if (string.IsNullOrWhiteSpace(manifest.ContentPackFor!.UniqueID)) + { + error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; + return false; + } } // validate required fields @@ -69,24 +77,21 @@ namespace StardewModdingAPI.Toolkit.Framework return false; } - // validate dependencies + // validate dependency format foreach (IManifestDependency? dependency in manifest.Dependencies) { - // null dependency if (dependency == null) { error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}."; return false; } - // missing ID if (string.IsNullOrWhiteSpace(dependency.UniqueID)) { error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."; return false; } - // invalid ID if (!PathUtilities.IsSlug(dependency.UniqueID)) { error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 0b4fe3e9..9db9db99 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -126,7 +126,14 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // check for dll if it's supposed to have one + // validate manifest format + if (!ManifestValidator.TryValidateFields(mod.Manifest, out string manifestError)) + { + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}"); + continue; + } + + // check that DLL exists if applicable if (!string.IsNullOrEmpty(mod.Manifest.EntryDll) && validateFilesExist) { IFileLookup pathLookup = getFileLookup(mod.DirectoryPath); @@ -137,13 +144,6 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } } - - // validate manifest - if (!ManifestValidator.TryValidate(mod.Manifest, out string manifestError)) - { - mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}"); - continue; - } } // validate IDs are unique -- cgit From 0629f19698c9920e2988d96f316d227f97932df8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 Nov 2022 01:22:45 -0500 Subject: change new fields to hash sets & simplify sorting This makes the mod IDs case-insensitive (like the 'SuppressUpdateChecks' field), fixes a build error in unit tests, and avoids re-scanning the mod list multiple times. --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 50 +++++++++++++-------------- src/SMAPI/Framework/Models/SConfig.cs | 16 ++++----- src/SMAPI/Framework/SCore.cs | 28 +++++++++------ 3 files changed, 51 insertions(+), 43 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 8ef5e4a8..1080a888 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -242,12 +242,32 @@ namespace StardewModdingAPI.Framework.ModLoading } } + /// Apply preliminary overrides to the load order based on the SMAPI configuration. + /// The mods to process. + /// The mod IDs SMAPI should load before any other mods (except those needed to load them). + /// The mod IDs SMAPI should load after any other mods. + public IModMetadata[] ApplyLoadOrderOverrides(IModMetadata[] mods, HashSet modIdsToLoadEarly, HashSet modIdsToLoadLate) + { + if (!modIdsToLoadEarly.Any() && !modIdsToLoadLate.Any()) + return mods; + + return mods + .OrderBy(mod => + { + string id = mod.Manifest.UniqueID; + if (modIdsToLoadEarly.Contains(id)) + return -1; + if (modIdsToLoadLate.Contains(id)) + return 1; + return 0; + }) + .ToArray(); + } + /// Sort the given mods by the order they should be loaded. /// The mods to process. /// Handles access to SMAPI's internal mod metadata list. - /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. - /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. - public IEnumerable ProcessDependencies(IEnumerable mods, IReadOnlyList modIdsToLoadEarly, IReadOnlyList modIdsToLoadLate, ModDatabase modDatabase) + public IEnumerable ProcessDependencies(IReadOnlyList mods, ModDatabase modDatabase) { // initialize metadata mods = mods.ToArray(); @@ -262,28 +282,8 @@ namespace StardewModdingAPI.Framework.ModLoading } // sort mods - IModMetadata[] allMods = mods.ToArray(); - IModMetadata[] modsToLoadEarly = modIdsToLoadEarly - .Where(modId => !modIdsToLoadLate.Contains(modId)) - .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId)) - .Where(m => m != null) - .Select(m => m!) - .ToArray(); - IModMetadata[] modsToLoadLate = modIdsToLoadLate - .Where(modId => !modIdsToLoadEarly.Contains(modId)) - .Select(modId => allMods.FirstOrDefault(m => m.Manifest.UniqueID == modId)) - .Where(m => m != null) - .Select(m => m!) - .ToArray(); - IModMetadata[] modsToLoadAsUsual = allMods.Where(m => !modsToLoadEarly.Contains(m) && !modsToLoadLate.Contains(m)).ToArray(); - - List orderSortedMods = new(); - orderSortedMods.AddRange(modsToLoadEarly); - orderSortedMods.AddRange(modsToLoadAsUsual); - orderSortedMods.AddRange(modsToLoadLate); - - foreach (IModMetadata mod in orderSortedMods) - this.ProcessDependencies(orderSortedMods, modDatabase, mod, states, sortedMods, new List()); + foreach (IModMetadata mod in mods) + this.ProcessDependencies(mods, modDatabase, mod, states, sortedMods, new List()); return sortedMods.Reverse(); } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 40d3450f..ee2dc18d 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public HashSet SuppressUpdateChecks { get; set; } - /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. - public List ModsToLoadEarly { get; set; } + /// The mod IDs SMAPI should load before any other mods (except those needed to load them). + public HashSet ModsToLoadEarly { get; set; } - /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. - public List ModsToLoadLate { get; set; } + /// The mod IDs SMAPI should load after any other mods. + public HashSet ModsToLoadLate { get; set; } /******** @@ -106,8 +106,8 @@ namespace StardewModdingAPI.Framework.Models /// The colors to use for text written to the SMAPI console. /// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - /// The mod IDs SMAPI should try to load early, before any other mods not included in this list. - /// The mod IDs SMAPI should try to load late, after all other mods not included in this list. + /// The mod IDs SMAPI should load before any other mods (except those needed to load them). + /// The mod IDs SMAPI should load after any other mods. public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate) { this.DeveloperMode = developerMode; @@ -123,8 +123,8 @@ namespace StardewModdingAPI.Framework.Models this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); - this.ModsToLoadEarly = new List(modsToLoadEarly ?? Array.Empty()); - this.ModsToLoadLate = new List(modsToLoadLate ?? Array.Empty()); + this.ModsToLoadEarly = new HashSet(modsToLoadEarly ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + this.ModsToLoadLate = new HashSet(modsToLoadLate ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); } /// Override the value of . diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 4d1eb959..fb835002 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -424,19 +424,27 @@ namespace StardewModdingAPI.Framework mods = mods.Where(p => !p.IsIgnored).ToArray(); // warn about mods that should load early or late which are not found at all, or both - foreach (string modId in this.Settings.ModsToLoadEarly) - if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found or was skipped.", LogLevel.Warn); - foreach (string modId in this.Settings.ModsToLoadLate) - if (!mods.Any(m => m.Manifest.UniqueID == modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn); - foreach (string modId in this.Settings.ModsToLoadEarly) - if (this.Settings.ModsToLoadLate.Contains(modId)) - this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn); + { + HashSet installedIds = new HashSet(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase); + + foreach (string modId in this.Settings.ModsToLoadEarly) + { + if (!installedIds.Contains(modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load early, but it could not be found or was skipped.", LogLevel.Warn); + } + foreach (string modId in this.Settings.ModsToLoadLate) + { + if (this.Settings.ModsToLoadEarly.Contains(modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load both early and late - this will be ignored.", LogLevel.Warn); + else if (!installedIds.Contains(modId)) + this.Monitor.Log($" SMAPI configuration specifies a mod {modId} that should load late, but it could not be found or was skipped.", LogLevel.Warn); + } + } // load mods resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); - mods = resolver.ProcessDependencies(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate, modDatabase).ToArray(); + mods = resolver.ApplyLoadOrderOverrides(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate); + mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // check for software likely to cause issues -- cgit From c1e3b25dcaff2406fa1e3a457c1ba9c0f8ecda7f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 Nov 2022 21:43:42 -0500 Subject: fix load-early/late mods not correctly sorted relative to others in the same list --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 96975e05..cb62e16f 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -174,14 +174,20 @@ namespace StardewModdingAPI.Framework.ModLoading if (!modIdsToLoadEarly.Any() && !modIdsToLoadLate.Any()) return mods; + string[] earlyArray = modIdsToLoadEarly.ToArray(); + string[] lateArray = modIdsToLoadLate.ToArray(); + return mods .OrderBy(mod => { string id = mod.Manifest.UniqueID; - if (modIdsToLoadEarly.Contains(id)) - return -1; - if (modIdsToLoadLate.Contains(id)) - return 1; + + if (modIdsToLoadEarly.TryGetValue(id, out string? actualId)) + return -int.MaxValue + Array.IndexOf(earlyArray, actualId); + + if (modIdsToLoadLate.TryGetValue(id, out actualId)) + return int.MaxValue - Array.IndexOf(lateArray, actualId); + return 0; }) .ToArray(); -- cgit