diff --git a/docs/release-notes.md b/docs/release-notes.md
index 748651bb..2ed46af1 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -9,6 +9,7 @@
## Upcoming release
* For players:
+ * When many mods fail to load, root dependencies are now listed in their own group so it's easier to see which ones you should try updating first.
* On macOS, the `StardewModdingAPI.bin.osx` file is no longer overwritten if it's identical to avoid resetting file permissions (thanks to 007wayne!).
* Fixed error for non-English players after returning to title, reloading, and entering town with a completed movie theater.
* Fixed `world_clear` console command not removing resource clumps outside the farm and secret woods.
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index 5d2f352d..f5babafb 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -117,6 +117,11 @@ namespace StardewModdingAPI.Framework
/// Only return valid update keys.
IEnumerable GetUpdateKeys(bool validOnly = true);
+ /// Get whether the given mod ID must be installed to load this mod.
+ /// The mod ID to check.
+ /// Whether to include optional dependencies.
+ bool HasRequiredModId(string modId, bool includeOptional);
+
/// Get the mod IDs that must be installed to load this mod.
/// Whether to include optional dependencies.
IEnumerable GetRequiredModIds(bool includeOptional = false);
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 38d561e5..4ba4fffc 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -429,67 +429,38 @@ namespace StardewModdingAPI.Framework.Logging
// log skipped mods
if (skippedMods.Any())
{
- // get logging logic
- HashSet loggedDuplicateIds = new HashSet();
- void LogSkippedMod(IModMetadata mod)
- {
- string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}";
+ var loggedDuplicateIds = new HashSet();
- // handle duplicate mods
- // (log first duplicate only, don't show redundant version)
- if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest())
- {
- if (!loggedDuplicateIds.Add(mod.Manifest.UniqueID))
- return; // already logged
-
- message = $" - {mod.DisplayName} because {mod.Error}";
- }
-
- // log message
- this.Monitor.Log(message, LogLevel.Error);
- if (mod.ErrorDetails != null)
- this.Monitor.Log($" ({mod.ErrorDetails})");
- }
-
- // group mods
- List skippedDependencies = new List();
- List otherSkippedMods = new List();
- {
- // track broken dependencies
- HashSet skippedDependencyIds = new HashSet(StringComparer.OrdinalIgnoreCase);
- HashSet skippedModIds = new HashSet(from mod in skippedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase);
- foreach (IModMetadata mod in skippedMods)
- {
- foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds()))
- skippedDependencyIds.Add(requiredId);
- }
-
- // collect mod groups
- foreach (IModMetadata mod in skippedMods)
- {
- if (mod.HasID() && skippedDependencyIds.Contains(mod.Manifest.UniqueID))
- skippedDependencies.Add(mod);
- else
- otherSkippedMods.Add(mod);
- }
- }
-
- // log skipped mods
this.Monitor.Log(" Skipped mods", LogLevel.Error);
this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error);
this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error);
this.Monitor.Newline();
-
- if (skippedDependencies.Any())
+ foreach (var list in this.GroupFailedModsByPriority(skippedMods))
{
- foreach (IModMetadata mod in skippedDependencies.OrderBy(p => p.DisplayName))
- LogSkippedMod(mod);
- this.Monitor.Newline();
- }
+ if (list.Any())
+ {
+ foreach (IModMetadata mod in list.OrderBy(p => p.DisplayName))
+ {
+ string message = $" - {mod.DisplayName}{(" " + mod.Manifest?.Version?.ToString()).TrimEnd()} because {mod.Error}";
- foreach (IModMetadata mod in otherSkippedMods.OrderBy(p => p.DisplayName))
- LogSkippedMod(mod);
- this.Monitor.Newline();
+ // duplicate mod: log first one only, don't show redundant version
+ if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest())
+ {
+ if (loggedDuplicateIds.Add(mod.Manifest.UniqueID))
+ continue; // already logged
+
+ message = $" - {mod.DisplayName} because {mod.Error}";
+ }
+
+ // log message
+ this.Monitor.Log(message, LogLevel.Error);
+ if (mod.ErrorDetails != null)
+ this.Monitor.Log($" ({mod.ErrorDetails})");
+ }
+
+ this.Monitor.Newline();
+ }
+ }
}
// log warnings
@@ -561,6 +532,92 @@ namespace StardewModdingAPI.Framework.Logging
}
}
+ /// Group failed mods by the priority players should update them, where mods in earlier groups are more likely to fix multiple mods.
+ /// The failed mods to group.
+ private IEnumerable> GroupFailedModsByPriority(IList failedMods)
+ {
+ var failedOthers = failedMods.ToList();
+ var skippedModIds = new HashSet(from mod in failedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase);
+
+ // group B: dependencies which failed
+ var failedOtherDependencies = new List();
+ {
+ // get failed dependency IDs
+ var skippedDependencyIds = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (IModMetadata mod in failedMods)
+ {
+ foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds()))
+ skippedDependencyIds.Add(requiredId);
+ }
+
+ // group matching mods
+ this.FilterThrough(
+ fromList: failedOthers,
+ toList: failedOtherDependencies,
+ match: mod => mod.HasID() && skippedDependencyIds.Contains(mod.Manifest.UniqueID)
+ );
+ }
+
+ // group A: failed root dependencies which other dependencies need
+ var failedRootDependencies = new List();
+ {
+ var skippedDependencyIds = new HashSet(failedOtherDependencies.Select(p => p.Manifest.UniqueID));
+ this.FilterThrough(
+ fromList: failedOtherDependencies,
+ toList: failedRootDependencies,
+ match: mod =>
+ {
+ // has no failed dependency
+ foreach (string requiredId in mod.GetRequiredModIds())
+ {
+ if (skippedDependencyIds.Contains(requiredId))
+ return false;
+ }
+
+ // another dependency depends on this mod
+ bool isDependedOn = false;
+ foreach (IModMetadata other in failedOtherDependencies)
+ {
+ if (other.HasRequiredModId(mod.Manifest.UniqueID, includeOptional: false))
+ {
+ isDependedOn = true;
+ break;
+ }
+ }
+
+ return isDependedOn;
+ }
+ );
+ }
+
+ // return groups
+ return new[]
+ {
+ failedRootDependencies,
+ failedOtherDependencies,
+ failedOthers
+ };
+ }
+
+ /// Filter matching items from one list and add them to the other.
+ /// The list item type.
+ /// The list to filter.
+ /// The list to which to add filtered items.
+ /// Matches items to filter through.
+ private void FilterThrough(IList fromList, IList toList, Func match)
+ {
+ for (int i = 0; i < fromList.Count; i++)
+ {
+ TItem item = fromList[i];
+ if (match(item))
+ {
+ toList.Add(item);
+ fromList.RemoveAt(i);
+ i--;
+ }
+ }
+ }
+
/// Write a mod warning group to the console and log.
/// The mods to search.
/// Matches mods to include in the warning group.
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index b4de3d6c..0d89fd20 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// The non-error issues with the mod, including warnings suppressed by the data record.
private ModWarning ActualWarnings = ModWarning.None;
+ /// The mod IDs which are listed as a requirement by this mod. The value for each pair indicates whether the dependency is required (i.e. not an optional dependency).
+ private Lazy> Dependencies;
+
/*********
** Accessors
@@ -100,6 +103,8 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Manifest = manifest;
this.DataRecord = dataRecord;
this.IsIgnored = isIgnored;
+
+ this.Dependencies = new Lazy>(this.ExtractDependencies);
}
///
@@ -198,24 +203,22 @@ namespace StardewModdingAPI.Framework.ModLoading
}
}
+ ///
+ public bool HasRequiredModId(string modId, bool includeOptional)
+ {
+ return
+ this.Dependencies.Value.TryGetValue(modId, out bool isRequired)
+ && (includeOptional || isRequired);
+ }
+
///
public IEnumerable GetRequiredModIds(bool includeOptional = false)
{
- HashSet required = new HashSet(StringComparer.OrdinalIgnoreCase);
-
- // yield dependencies
- if (this.Manifest?.Dependencies != null)
+ foreach (var pair in this.Dependencies.Value)
{
- foreach (var entry in this.Manifest?.Dependencies)
- {
- if ((entry.IsRequired || includeOptional) && required.Add(entry.UniqueID))
- yield return entry.UniqueID;
- }
+ if (includeOptional || pair.Value)
+ yield return pair.Key;
}
-
- // yield content pack parent
- if (this.Manifest?.ContentPackFor?.UniqueID != null && required.Add(this.Manifest.ContentPackFor.UniqueID))
- yield return this.Manifest.ContentPackFor.UniqueID;
}
///
@@ -237,5 +240,29 @@ namespace StardewModdingAPI.Framework.ModLoading
string rootFolderName = Path.GetFileName(this.RootPath) ?? "";
return Path.Combine(rootFolderName, this.RelativeDirectoryPath);
}
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Extract mod IDs from the manifest that must be installed to load this mod.
+ /// Returns a dictionary of mod ID => is required (i.e. not an optional dependency).
+ public IDictionary ExtractDependencies()
+ {
+ var ids = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ // yield dependencies
+ if (this.Manifest?.Dependencies != null)
+ {
+ foreach (var entry in this.Manifest?.Dependencies)
+ ids[entry.UniqueID] = entry.IsRequired;
+ }
+
+ // yield content pack parent
+ if (this.Manifest?.ContentPackFor?.UniqueID != null)
+ ids[this.Manifest.ContentPackFor.UniqueID] = true;
+
+ return ids;
+ }
}
}