summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs76
-rw-r--r--src/SMAPI/Context.cs2
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs4
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs2
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs30
-rw-r--r--src/SMAPI/Framework/ContentPack.cs2
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs5
-rw-r--r--src/SMAPI/Framework/Logging/LogFileManager.cs2
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs194
-rw-r--r--src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs53
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs4
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs3
-rw-r--r--src/SMAPI/Framework/SCore.cs79
-rw-r--r--src/SMAPI/Framework/Serialization/ColorConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/PointConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/RectangleConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/Vector2Converter.cs2
-rw-r--r--src/SMAPI/GamePlatform.cs2
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs42
-rw-r--r--src/SMAPI/Program.cs4
-rw-r--r--src/SMAPI/SMAPI.config.json9
-rw-r--r--src/SMAPI/SMAPI.csproj6
24 files changed, 372 insertions, 159 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 8b0c952d..3c21b205 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -38,6 +38,14 @@ namespace StardewModdingAPI
/// <summary>The target game platform.</summary>
internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
+ /// <summary>Whether SMAPI is being compiled for Windows with a 64-bit Linux version of the game. This is highly specialized and shouldn't be used in most cases.</summary>
+ internal static bool IsWindows64BitHack { get; } =
+#if SMAPI_FOR_WINDOWS_64BIT_HACK
+ true;
+#else
+ false;
+#endif
+
/// <summary>The game framework running the game.</summary>
internal static GameFramework GameFramework { get; } =
#if SMAPI_FOR_XNA
@@ -47,10 +55,13 @@ namespace StardewModdingAPI
#endif
/// <summary>The game's assembly name.</summary>
- internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley";
+ internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows && !EarlyConstants.IsWindows64BitHack ? "Stardew Valley" : "StardewValley";
/// <summary>The <see cref="Context.ScreenId"/> value which should appear in the SMAPI log, if any.</summary>
internal static int? LogScreenId { get; set; }
+
+ /// <summary>SMAPI's current raw semantic version.</summary>
+ internal static string RawApiVersion = "3.10.0";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -63,7 +74,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.5");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion);
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4");
@@ -231,33 +242,27 @@ namespace StardewModdingAPI
targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly);
// get changes for platform
- switch (targetPlatform)
+ if (Constants.Platform != Platform.Windows || EarlyConstants.IsWindows64BitHack)
{
- case Platform.Linux:
- case Platform.Mac:
- removeAssemblyReferences.AddRange(new[]
- {
- "Netcode",
- "Stardew Valley"
- });
- targetAssemblies.Add(
- typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/Mac
- );
- break;
-
- case Platform.Windows:
- removeAssemblyReferences.Add(
- "StardewValley"
- );
- targetAssemblies.AddRange(new[]
- {
- typeof(Netcode.NetBool).Assembly,
- typeof(StardewValley.Game1).Assembly
- });
- break;
-
- default:
- throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'.");
+ removeAssemblyReferences.AddRange(new[]
+ {
+ "Netcode",
+ "Stardew Valley"
+ });
+ targetAssemblies.Add(
+ typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/macOS
+ );
+ }
+ else
+ {
+ removeAssemblyReferences.Add(
+ "StardewValley"
+ );
+ targetAssemblies.AddRange(new[]
+ {
+ typeof(Netcode.NetBool).Assembly,
+ typeof(StardewValley.Game1).Assembly
+ });
}
// get changes for game framework
@@ -295,6 +300,21 @@ namespace StardewModdingAPI
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray());
}
+ /// <summary>Get whether the game assembly was patched by Stardew64Installer.</summary>
+ /// <param name="version">The version of Stardew64Installer which was applied to the game assembly, if any.</param>
+ internal static bool IsPatchedByStardew64Installer(out ISemanticVersion version)
+ {
+ PropertyInfo property = typeof(Game1).GetProperty("Stardew64InstallerVersion");
+ if (property == null)
+ {
+ version = null;
+ return false;
+ }
+
+ version = new SemanticVersion((string)property.GetValue(null));
+ return true;
+ }
+
/*********
** Private methods
diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs
index 5f70d0f7..a745592c 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -86,7 +86,7 @@ namespace StardewModdingAPI
public static bool HasRemotePlayers => Context.IsMultiplayer && !Game1.hasLocalClientsOnly;
/// <summary>Whether the current player is the main player. This is always true in single-player, and true when hosting in multiplayer.</summary>
- public static bool IsMainPlayer => Game1.IsMasterGame && !(TitleMenu.subMenu is FarmhandMenu);
+ public static bool IsMainPlayer => Game1.IsMasterGame && Context.ScreenId == 0 && !(TitleMenu.subMenu is FarmhandMenu);
/*********
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index 5f91610e..529fb93a 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -12,7 +12,7 @@ namespace StardewModdingAPI.Framework.Content
** Fields
*********/
/// <summary>The minimum value to consider non-transparent.</summary>
- /// <remarks>On Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason.</remarks>
+ /// <remarks>On Linux/macOS, fully transparent pixels may have an alpha up to 4 for some reason.</remarks>
private const byte MinOpacity = 5;
@@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.Content
// premultiplied by the content pipeline. The formula is derived from
// https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/.
// Note: don't use named arguments here since they're different between
- // Linux/Mac and Windows.
+ // Linux/macOS and Windows.
float alphaBelow = 1 - (above.A / 255f);
newData[i] = new Color(
(int)(above.R + (below.R * alphaBelow)), // r
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index 7edc9ab9..5c7ad778 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -57,6 +57,8 @@ namespace StardewModdingAPI.Framework.Content
IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath");
this.NormalizeAssetNameForPlatform = path => method.Invoke<string>(path);
}
+ else if (EarlyConstants.IsWindows64BitHack)
+ this.NormalizeAssetNameForPlatform = PathUtilities.NormalizePath;
else
this.NormalizeAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 2920e670..d0e759c2 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -132,7 +132,7 @@ namespace StardewModdingAPI.Framework
);
this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
- this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, reflection, aggressiveMemoryOptimizations);
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, aggressiveMemoryOptimizations);
}
/// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 9af14cb5..4f6aa775 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The language code for language-agnostic mod assets.</summary>
private readonly LanguageCode DefaultLanguage = Constants.DefaultLanguage;
+ /// <summary>If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder.</summary>
+ private readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" };
+
/*********
** Public methods
@@ -215,11 +218,17 @@ namespace StardewModdingAPI.Framework.ContentManagers
FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path));
// try with default extension
- if (!file.Exists && file.Extension.ToLower() != ".xnb")
+ if (!file.Exists && file.Extension == string.Empty)
{
- FileInfo result = new FileInfo(file.FullName + ".xnb");
- if (result.Exists)
- file = result;
+ foreach (string extension in this.LocalTilesheetExtensions)
+ {
+ FileInfo result = new FileInfo(file.FullName + extension);
+ if (result.Exists)
+ {
+ file = result;
+ break;
+ }
+ }
}
return file;
@@ -259,6 +268,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder
// fix tilesheets
+ this.Monitor.VerboseLog($"Fixing tilesheet paths for map '{relativeMapPath}' from mod '{this.ModName}'...");
foreach (TileSheet tilesheet in map.TileSheets)
{
// get image source
@@ -280,6 +290,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out string assetName, out string error))
throw new SContentLoadException($"{errorPrefix} {error}");
+ if (assetName != tilesheet.ImageSource)
+ this.Monitor.VerboseLog($" Mapped tilesheet '{tilesheet.ImageSource}' to '{assetName}'.");
+
tilesheet.ImageSource = assetName;
}
catch (Exception ex) when (!(ex is SContentLoadException))
@@ -308,6 +321,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
return true;
}
+ // special case: local filenames starting with a dot should be ignored
+ // For example, this lets mod authors have a '.spring_town.png' file in their map folder so it can be
+ // opened in Tiled, while still mapping it to the vanilla 'Maps/spring_town' asset at runtime.
+ {
+ string filename = Path.GetFileName(relativePath);
+ if (filename.StartsWith("."))
+ relativePath = Path.Combine(Path.GetDirectoryName(relativePath) ?? "", filename.TrimStart('.'));
+ }
+
// get relative to map file
{
string localKey = Path.Combine(modRelativeMapFolder, relativePath);
diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index a6835dbe..0660a367 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
- /// <summary>A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/Mac.</summary>
+ /// <summary>A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/macOS.</summary>
private readonly IDictionary<string, string> RelativePaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
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
/// <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);
diff --git a/src/SMAPI/Framework/Logging/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs
index 6b5babcd..6ab2bdfb 100644
--- a/src/SMAPI/Framework/Logging/LogFileManager.cs
+++ b/src/SMAPI/Framework/Logging/LogFileManager.cs
@@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Logging
public void WriteLine(string message)
{
// always use Windows-style line endings for convenience
- // (Linux/Mac editors are fine with them, Windows editors often require them)
+ // (Linux/macOS editors are fine with them, Windows editors often require them)
this.Stream.Write(message + "\r\n");
}
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 243ca3ae..a4df3c18 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -283,13 +283,16 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="customSettings">The custom SMAPI settings.</param>
public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
{
- // get platform label
- string platformLabel = EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform);
- if ((Constants.GameFramework == GameFramework.Xna) != (Constants.Platform == Platform.Windows))
- platformLabel += $" with {Constants.GameFramework}";
+ // log platform & patches
+ {
+ this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
+
+ string[] patchLabels = this.GetPatchLabels().ToArray();
+ if (patchLabels.Any())
+ this.Monitor.Log($"Detected custom version: {string.Join(", ", patchLabels)}", LogLevel.Info);
+ }
- // init logging
- this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {platformLabel}", LogLevel.Info);
+ // log basic info
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
if (modsPath != Constants.DefaultModsPath)
this.Monitor.Log("(Using custom --mods-path argument.)");
@@ -406,6 +409,20 @@ namespace StardewModdingAPI.Framework.Logging
gameMonitor.Log(message, level);
}
+ /// <summary>Get human-readable labels to log for detected SMAPI and Stardew Valley customizations.</summary>
+ private IEnumerable<string> GetPatchLabels()
+ {
+ // custom game framework
+ if (EarlyConstants.IsWindows64BitHack)
+ yield return $"running 64-bit SMAPI with {Constants.GameFramework}";
+ else if ((Constants.GameFramework == GameFramework.Xna) != (Constants.Platform == Platform.Windows))
+ yield return $"running {Constants.GameFramework}";
+
+ // patched by Stardew64Installer
+ if (Constants.IsPatchedByStardew64Installer(out ISemanticVersion patchedByVersion))
+ yield return $"patched by Stardew64Installer {patchedByVersion}";
+ }
+
/// <summary>Write a summary of mod warnings to the console and log.</summary>
/// <param name="mods">The loaded mods.</param>
/// <param name="skippedMods">The mods which could not be loaded.</param>
@@ -426,67 +443,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())
+ 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();
+ foreach (var list in this.GroupFailedModsByPriority(skippedMods))
+ {
+ if (list.Any())
{
- if (!loggedDuplicateIds.Add(mod.Manifest.UniqueID))
- return; // already logged
+ foreach (IModMetadata mod in list.OrderBy(p => p.DisplayName))
+ {
+ string message = $" - {mod.DisplayName}{(" " + mod.Manifest?.Version?.ToString()).TrimEnd()} because {mod.Error}";
- message = $" - {mod.DisplayName} because {mod.Error}";
- }
+ // 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
- // log message
- this.Monitor.Log(message, LogLevel.Error);
- if (mod.ErrorDetails != null)
- this.Monitor.Log($" ({mod.ErrorDetails})");
- }
+ message = $" - {mod.DisplayName} because {mod.Error}";
+ }
- // 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);
- }
+ // log message
+ this.Monitor.Log(message, LogLevel.Error);
+ if (mod.ErrorDetails != null)
+ this.Monitor.Log($" ({mod.ErrorDetails})");
+ }
- // collect mod groups
- foreach (IModMetadata mod in skippedMods)
- {
- if (mod.HasID() && skippedDependencyIds.Contains(mod.Manifest.UniqueID))
- skippedDependencies.Add(mod);
- else
- otherSkippedMods.Add(mod);
+ this.Monitor.Newline();
}
}
-
- // 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 (IModMetadata mod in skippedDependencies.OrderBy(p => p.DisplayName))
- LogSkippedMod(mod);
- this.Monitor.Newline();
- }
-
- foreach (IModMetadata mod in otherSkippedMods.OrderBy(p => p.DisplayName))
- LogSkippedMod(mod);
- this.Monitor.Newline();
}
// log warnings
@@ -553,9 +541,95 @@ namespace StardewModdingAPI.Framework.Logging
// not crossplatform
this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform",
- "These mods use the 'dynamic' keyword, and won't work on Linux/Mac."
+ "These mods use the 'dynamic' keyword, and won't work on Linux/macOS."
+ );
+ }
+ }
+
+ /// <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>
diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
index a948213b..baffc50e 100644
--- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
+++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod.</summary>
DetectedSaveSerializer,
- /// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
+ /// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary>
DetectedDynamic,
/// <summary>The instruction is compatible, but references <see cref="ISpecializedEvents.UnvalidatedUpdateTicking"/> or <see cref="ISpecializedEvents.UnvalidatedUpdateTicked"/> which may impact stability.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index b4de3d6c..17e6d59a 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -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 readonly 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 />
@@ -199,23 +204,21 @@ namespace StardewModdingAPI.Framework.ModLoading
}
/// <inheritdoc />
- public IEnumerable<string> GetRequiredModIds(bool includeOptional = false)
+ public bool HasRequiredModId(string modId, bool includeOptional)
{
- HashSet<string> required = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ return
+ this.Dependencies.Value.TryGetValue(modId, out bool isRequired)
+ && (includeOptional || isRequired);
+ }
- // yield dependencies
- if (this.Manifest?.Dependencies != null)
+ /// <inheritdoc />
+ public IEnumerable<string> GetRequiredModIds(bool includeOptional = false)
+ {
+ 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.Ordi