diff options
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModResolver.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/SConfig.cs | 18 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 23 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.config.json | 14 |
4 files changed, 78 insertions, 7 deletions
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9db9db99..96975e05 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -165,10 +165,32 @@ namespace StardewModdingAPI.Framework.ModLoading } } + /// <summary>Apply preliminary overrides to the load order based on the SMAPI configuration.</summary> + /// <param name="mods">The mods to process.</param> + /// <param name="modIdsToLoadEarly">The mod IDs SMAPI should load before any other mods (except those needed to load them).</param> + /// <param name="modIdsToLoadLate">The mod IDs SMAPI should load after any other mods.</param> + public IModMetadata[] ApplyLoadOrderOverrides(IModMetadata[] mods, HashSet<string> modIdsToLoadEarly, HashSet<string> 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(); + } + /// <summary>Sort the given mods by the order they should be loaded.</summary> /// <param name="mods">The mods to process.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> - public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods, ModDatabase modDatabase) + public IEnumerable<IModMetadata> ProcessDependencies(IReadOnlyList<IModMetadata> mods, ModDatabase modDatabase) { // initialize metadata mods = mods.ToArray(); @@ -184,7 +206,7 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List<IModMetadata>()); + this.ProcessDependencies(mods, modDatabase, mod, states, sortedMods, new List<IModMetadata>()); return sortedMods.Reverse(); } @@ -201,7 +223,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param> /// <param name="currentChain">The current change of mod dependencies.</param> /// <returns>Returns the mod dependency status.</returns> - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain) + private ModDependencyStatus ProcessDependencies(IReadOnlyList<IModMetadata> mods, ModDatabase modDatabase, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain) { // check if already visited switch (states[mod]) @@ -332,7 +354,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Get the dependencies declared in a manifest.</summary> /// <param name="manifest">The mod manifest.</param> /// <param name="loadedMods">The loaded mods.</param> - private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IReadOnlyList<IModMetadata> 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 825ebb44..87970e6c 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -86,6 +86,12 @@ namespace StardewModdingAPI.Framework.Models /// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary> public HashSet<string> SuppressUpdateChecks { get; set; } + /// <summary>The mod IDs SMAPI should load before any other mods (except those needed to load them).</summary> + public HashSet<string> ModsToLoadEarly { get; set; } + + /// <summary>The mod IDs SMAPI should load after any other mods.</summary> + public HashSet<string> ModsToLoadLate { get; set; } + /******** ** Public methods @@ -105,7 +111,9 @@ namespace StardewModdingAPI.Framework.Models /// <param name="consoleColors"><inheritdoc cref="ConsoleColors" path="/summary" /></param> /// <param name="suppressHarmonyDebugMode"><inheritdoc cref="SuppressHarmonyDebugMode" path="/summary" /></param> /// <param name="suppressUpdateChecks"><inheritdoc cref="SuppressUpdateChecks" path="/summary" /></param> - public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks) + /// <param name="modsToLoadEarly"><inheritdoc cref="ModsToLoadEarly" path="/summary" /></param> + /// <param name="modsToLoadLate"><inheritdoc cref="ModsToLoadLate" path="/summary" /></param> + public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, 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)]; @@ -121,6 +129,8 @@ namespace StardewModdingAPI.Framework.Models this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet<string>(suppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase); + this.ModsToLoadEarly = new HashSet<string>(modsToLoadEarly ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase); + this.ModsToLoadLate = new HashSet<string>(modsToLoadLate ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase); } /// <summary>Override the value of <see cref="DeveloperMode"/>.</summary> @@ -142,6 +152,12 @@ namespace StardewModdingAPI.Framework.Models custom[name] = value; } + if (this.ModsToLoadEarly.Any()) + custom[nameof(this.ModsToLoadEarly)] = $"[{string.Join(", ", this.ModsToLoadEarly)}]"; + + if (this.ModsToLoadLate.Any()) + custom[nameof(this.ModsToLoadLate)] = $"[{string.Join(", ", this.ModsToLoadLate)}]"; + if (!this.SuppressUpdateChecks.SetEquals(SConfig.DefaultSuppressUpdateChecks)) custom[nameof(this.SuppressUpdateChecks)] = $"[{string.Join(", ", this.SuppressUpdateChecks)}]"; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 7bb54653..8e21e7dc 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,8 +423,29 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); mods = mods.Where(p => !p.IsIgnored).ToArray(); - // load mods + // validate manifests resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFileLookup: this.GetFileLookup); + + // apply load order customizations + if (this.Settings.ModsToLoadEarly.Any() || this.Settings.ModsToLoadLate.Any()) + { + HashSet<string> installedIds = new HashSet<string>(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase); + + string[] missingEarlyMods = this.Settings.ModsToLoadEarly.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray(); + string[] missingLateMods = this.Settings.ModsToLoadLate.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray(); + string[] duplicateMods = this.Settings.ModsToLoadLate.Where(id => this.Settings.ModsToLoadEarly.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray(); + + if (missingEarlyMods.Any()) + this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadEarly)} which aren't installed: '{string.Join("', '", missingEarlyMods)}'.", LogLevel.Warn); + if (missingLateMods.Any()) + this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs in {nameof(this.Settings.ModsToLoadLate)} which aren't installed: '{string.Join("', '", missingLateMods)}'.", LogLevel.Warn); + if (duplicateMods.Any()) + this.Monitor.Log($" The 'smapi-internal/config.json' file lists mod IDs which are in both {nameof(this.Settings.ModsToLoadEarly)} and {nameof(this.Settings.ModsToLoadLate)}: '{string.Join("', '", duplicateMods)}'. These will be loaded early.", LogLevel.Warn); + + mods = resolver.ApplyLoadOrderOverrides(mods, this.Settings.ModsToLoadEarly, this.Settings.ModsToLoadLate); + } + + // load mods mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 52f25fdd..0d00db4d 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -147,5 +147,17 @@ 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 load before any other mods (except those needed to load them) + * or after any other mods. + * + * This lets you manually fix the load order if needed, but this is a last resort — SMAPI + * automatically adjusts the load order based on mods' dependencies, so needing to manually + * edit the order is usually a problem with one or both mods' metadata that can be reported to + * the mod author. + */ + "ModsToLoadEarly": [], + "ModsToLoadLate": [] } |