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": []
}