log failed root dependencies in their own group
This commit is contained in:
parent
60b563267e
commit
2d8f916053
|
@ -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.
|
||||
|
|
|
@ -117,6 +117,11 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="validOnly">Only return valid update keys.</param>
|
||||
IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = true);
|
||||
|
||||
/// <summary>Get whether the given mod ID must be installed to load this mod.</summary>
|
||||
/// <param name="modId">The mod ID to check.</param>
|
||||
/// <param name="includeOptional">Whether to include optional dependencies.</param>
|
||||
bool HasRequiredModId(string modId, bool includeOptional);
|
||||
|
||||
/// <summary>Get the mod IDs that must be installed to load this mod.</summary>
|
||||
/// <param name="includeOptional">Whether to include optional dependencies.</param>
|
||||
IEnumerable<string> GetRequiredModIds(bool includeOptional = false);
|
||||
|
|
|
@ -429,67 +429,38 @@ namespace StardewModdingAPI.Framework.Logging
|
|||
// log skipped mods
|
||||
if (skippedMods.Any())
|
||||
{
|
||||
// get logging logic
|
||||
HashSet<string> loggedDuplicateIds = new HashSet<string>();
|
||||
void LogSkippedMod(IModMetadata mod)
|
||||
{
|
||||
string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}";
|
||||
var loggedDuplicateIds = new HashSet<string>();
|
||||
|
||||
// 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<IModMetadata> skippedDependencies = new List<IModMetadata>();
|
||||
List<IModMetadata> otherSkippedMods = new List<IModMetadata>();
|
||||
{
|
||||
// track broken dependencies
|
||||
HashSet<string> skippedDependencyIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
HashSet<string> skippedModIds = new HashSet<string>(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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Group failed mods by the priority players should update them, where mods in earlier groups are more likely to fix multiple mods.</summary>
|
||||
/// <param name="failedMods">The failed mods to group.</param>
|
||||
private IEnumerable<IList<IModMetadata>> GroupFailedModsByPriority(IList<IModMetadata> failedMods)
|
||||
{
|
||||
var failedOthers = failedMods.ToList();
|
||||
var skippedModIds = new HashSet<string>(from mod in failedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// group B: dependencies which failed
|
||||
var failedOtherDependencies = new List<IModMetadata>();
|
||||
{
|
||||
// get failed dependency IDs
|
||||
var skippedDependencyIds = new HashSet<string>(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<IModMetadata>();
|
||||
{
|
||||
var skippedDependencyIds = new HashSet<string>(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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Filter matching items from one list and add them to the other.</summary>
|
||||
/// <typeparam name="TItem">The list item type.</typeparam>
|
||||
/// <param name="fromList">The list to filter.</param>
|
||||
/// <param name="toList">The list to which to add filtered items.</param>
|
||||
/// <param name="match">Matches items to filter through.</param>
|
||||
private void FilterThrough<TItem>(IList<TItem> fromList, IList<TItem> toList, Func<TItem, bool> match)
|
||||
{
|
||||
for (int i = 0; i < fromList.Count; i++)
|
||||
{
|
||||
TItem item = fromList[i];
|
||||
if (match(item))
|
||||
{
|
||||
toList.Add(item);
|
||||
fromList.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Write a mod warning group to the console and log.</summary>
|
||||
/// <param name="mods">The mods to search.</param>
|
||||
/// <param name="match">Matches mods to include in the warning group.</param>
|
||||
|
|
|
@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
/// <summary>The non-error issues with the mod, including warnings suppressed by the data record.</summary>
|
||||
private ModWarning ActualWarnings = ModWarning.None;
|
||||
|
||||
/// <summary>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).</summary>
|
||||
private Lazy<IDictionary<string, bool>> Dependencies;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
|
@ -100,6 +103,8 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
this.Manifest = manifest;
|
||||
this.DataRecord = dataRecord;
|
||||
this.IsIgnored = isIgnored;
|
||||
|
||||
this.Dependencies = new Lazy<IDictionary<string, bool>>(this.ExtractDependencies);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -198,24 +203,22 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasRequiredModId(string modId, bool includeOptional)
|
||||
{
|
||||
return
|
||||
this.Dependencies.Value.TryGetValue(modId, out bool isRequired)
|
||||
&& (includeOptional || isRequired);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetRequiredModIds(bool includeOptional = false)
|
||||
{
|
||||
HashSet<string> required = new HashSet<string>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -237,5 +240,29 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
string rootFolderName = Path.GetFileName(this.RootPath) ?? "";
|
||||
return Path.Combine(rootFolderName, this.RelativeDirectoryPath);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Extract mod IDs from the manifest that must be installed to load this mod.</summary>
|
||||
/// <returns>Returns a dictionary of mod ID => is required (i.e. not an optional dependency).</returns>
|
||||
public IDictionary<string, bool> ExtractDependencies()
|
||||
{
|
||||
var ids = new Dictionary<string, bool>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue