summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs300
-rw-r--r--src/StardewModdingAPI/Framework/ModRegistry.cs28
-rw-r--r--src/StardewModdingAPI/Program.cs250
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.csproj1
4 files changed, 304 insertions, 275 deletions
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
new file mode 100644
index 00000000..450fe6bf
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
@@ -0,0 +1,300 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using StardewModdingAPI.Framework.Models;
+using StardewModdingAPI.Framework.Serialisation;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Finds and processes mod metadata.</summary>
+ internal class ModResolver
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ private readonly IMonitor Monitor;
+
+ /// <summary>Manages deprecation warnings.</summary>
+ private readonly DeprecationManager DeprecationManager;
+
+ /// <summary>Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ private readonly ModCompatibility[] CompatibilityRecords;
+
+
+ /*********
+ ** Public methods
+ *********/
+ public ModResolver(IMonitor monitor, DeprecationManager deprecationManager, IEnumerable<ModCompatibility> compatibilityRecords)
+ {
+ this.Monitor = monitor;
+ this.DeprecationManager = deprecationManager;
+ this.CompatibilityRecords = compatibilityRecords.ToArray();
+ }
+
+ /// <summary>Find all mods in the given folder.</summary>
+ /// <param name="rootPath">The root mod path to search.</param>
+ /// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param>
+ /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
+ public ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList<Action> deprecationWarnings)
+ {
+ this.Monitor.Log("Finding mods...");
+ void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level);
+
+ // load mod metadata
+ List<ModMetadata> mods = new List<ModMetadata>();
+ foreach (string modRootPath in Directory.GetDirectories(rootPath))
+ {
+ if (this.Monitor.IsExiting)
+ return new ModMetadata[0]; // exit in progress
+
+ // init metadata
+ string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\');
+
+ // passthrough empty directories
+ DirectoryInfo directory = new DirectoryInfo(modRootPath);
+ while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
+ directory = directory.GetDirectories().First();
+
+ // get manifest path
+ string manifestPath = Path.Combine(directory.FullName, "manifest.json");
+ if (!File.Exists(manifestPath))
+ {
+ LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn);
+ continue;
+ }
+
+ // read manifest
+ Manifest manifest;
+ try
+ {
+ // read manifest file
+ string json = File.ReadAllText(manifestPath);
+ if (string.IsNullOrEmpty(json))
+ {
+ LogSkip(displayName, "its manifest is empty.");
+ continue;
+ }
+
+ // parse manifest
+ manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
+ if (manifest == null)
+ {
+ LogSkip(displayName, "its manifest is invalid.");
+ continue;
+ }
+
+ // validate manifest
+ if (string.IsNullOrWhiteSpace(manifest.EntryDll))
+ {
+ LogSkip(displayName, "its manifest doesn't set an entry DLL.");
+ continue;
+ }
+ if (string.IsNullOrWhiteSpace(manifest.UniqueID))
+ deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
+ }
+ catch (Exception ex)
+ {
+ LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}");
+ continue;
+ }
+ if (!string.IsNullOrWhiteSpace(manifest.Name))
+ displayName = manifest.Name;
+
+ // validate compatibility
+ ModCompatibility compatibility = this.GetCompatibilityRecord(manifest);
+ if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
+ {
+ bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
+ bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
+
+ string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
+ string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
+ if (hasOfficialUrl)
+ error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
+ if (hasUnofficialUrl)
+ error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
+
+ LogSkip(displayName, error);
+ }
+
+ // validate SMAPI version
+ if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
+ {
+ try
+ {
+ ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
+ if (minVersion.IsNewerThan(Constants.ApiVersion))
+ {
+ LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
+ continue;
+ }
+ }
+ catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
+ {
+ LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
+ continue;
+ }
+ }
+
+ // create per-save directory
+ if (manifest.PerSaveConfigs)
+ {
+ deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
+ try
+ {
+ string psDir = Path.Combine(directory.FullName, "psconfigs");
+ Directory.CreateDirectory(psDir);
+ if (!Directory.Exists(psDir))
+ {
+ LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
+ continue;
+ }
+ }
+
+ // validate DLL path
+ string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
+ if (!File.Exists(assemblyPath))
+ {
+ LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist.");
+ continue;
+ }
+
+ // add mod metadata
+ mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility));
+ }
+
+ return this.HandleModDependencies(mods.ToArray());
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ /// <param name="manifest">The mod manifest.</param>
+ /// <returns>Returns the incompatibility record if applicable, else <c>null</c>.</returns>
+ private ModCompatibility GetCompatibilityRecord(IManifest manifest)
+ {
+ string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
+ return (
+ from mod in this.CompatibilityRecords
+ where
+ mod.ID == key
+ && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
+ && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
+ select mod
+ ).FirstOrDefault();
+ }
+
+ /// <summary>Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
+ /// <param name="mods">The mods to process.</param>
+ private ModMetadata[] HandleModDependencies(ModMetadata[] mods)
+ {
+ this.Monitor.Log("Checking mod dependencies...");
+ var unsortedMods = mods.ToList();
+ var sortedMods = new Stack<ModMetadata>();
+ var visitedMods = new bool[unsortedMods.Count];
+ var currentChain = new List<ModMetadata>();
+ bool success = true;
+
+ for (int index = 0; index < unsortedMods.Count; index++)
+ {
+ success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
+ if (!success)
+ break;
+ }
+
+ if (!success)
+ {
+ // Failed to sort list, return no mods.
+ this.Monitor.Log("No mods will be loaded.", LogLevel.Error);
+ return new ModMetadata[0];
+ }
+
+ return sortedMods.Reverse().ToArray();
+ }
+
+ /// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
+ /// <param name="modIndex">The index of the mod being processed in the <paramref name="unsortedMods"/>.</param>
+ /// <param name="visitedMods">The mods which have been processed.</param>
+ /// <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>
+ /// <param name="unsortedMods">The mods remaining to sort.</param>
+ /// <returns>Returns whether the mod can be loaded.</returns>
+ private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack<ModMetadata> sortedMods, List<ModMetadata> currentChain, List<ModMetadata> unsortedMods)
+ {
+ // visit mod
+ if (visitedMods[modIndex])
+ return true; // already sorted
+ ModMetadata mod = unsortedMods[modIndex];
+ visitedMods[modIndex] = true;
+
+ // process dependencies
+ bool success = true;
+ if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any())
+ {
+ // validate required dependencies are present
+ {
+ string missingMods = null;
+ foreach (IManifestDependency dependency in mod.Manifest.Dependencies)
+ {
+ if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID)))
+ missingMods += $"{dependency.UniqueID}, ";
+ }
+ if (missingMods != null)
+ {
+ this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error);
+ return false;
+ }
+ }
+
+ // get mods which should be loaded before this one
+ ModMetadata[] modsToLoadFirst =
+ (
+ from unsorted in unsortedMods
+ where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID)
+ select unsorted
+ )
+ .ToArray();
+
+ // detect circular references
+ ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains);
+ if (circularReferenceMod != null)
+ {
+ this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error);
+ string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}";
+ for (int i = currentChain.Count - 1; i >= 0; i--)
+ {
+ chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain;
+ if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break;
+ }
+ this.Monitor.Log(chain, LogLevel.Error);
+ return false;
+ }
+ currentChain.Add(mod);
+
+ // recursively sort dependencies
+ foreach (ModMetadata requiredMod in modsToLoadFirst)
+ {
+ int index = unsortedMods.IndexOf(requiredMod);
+ success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
+ if (!success)
+ break;
+ }
+ }
+
+ // mark mod sorted
+ sortedMods.Push(mod);
+ currentChain.Remove(mod);
+ return success;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs
index f015b7ba..3899aa3f 100644
--- a/src/StardewModdingAPI/Framework/ModRegistry.cs
+++ b/src/StardewModdingAPI/Framework/ModRegistry.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
-using StardewModdingAPI.Framework.Models;
namespace StardewModdingAPI.Framework
{
@@ -19,21 +18,10 @@ namespace StardewModdingAPI.Framework
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
- /// <summary>Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
- private readonly ModCompatibility[] CompatibilityRecords;
-
/*********
** Public methods
*********/
- /// <summary>Construct an instance.</summary>
- /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
- public ModRegistry(IEnumerable<ModCompatibility> compatibilityRecords)
- {
- this.CompatibilityRecords = compatibilityRecords.ToArray();
- }
-
-
/****
** IModRegistry
****/
@@ -125,21 +113,5 @@ namespace StardewModdingAPI.Framework
// no known assembly found
return null;
}
-
- /// <summary>Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.</summary>
- /// <param name="manifest">The mod manifest.</param>
- /// <returns>Returns the incompatibility record if applicable, else <c>null</c>.</returns>
- internal ModCompatibility GetCompatibilityRecord(IManifest manifest)
- {
- string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
- return (
- from mod in this.CompatibilityRecords
- where
- mod.ID == key
- && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
- && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
- select mod
- ).FirstOrDefault();
- }
}
}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index a86a9540..7b421895 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -269,7 +269,7 @@ namespace StardewModdingAPI
this.GameInstance.VerboseLogging = this.Settings.VerboseLogging;
// load core components
- this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility);
+ this.ModRegistry = new ModRegistry();
this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
this.CommandManager = new CommandManager();
@@ -316,8 +316,8 @@ namespace StardewModdingAPI
// load mods
JsonHelper jsonHelper = new JsonHelper();
IList<Action> deprecationWarnings = new List<Action>();
- ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings);
- mods = this.HandleModDependencies(mods);
+ ModMetadata[] mods = new ModResolver(this.Monitor, this.DeprecationManager, this.Settings.ModCompatibility)
+ .FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings);
modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings);
// log deprecation warnings together
@@ -338,109 +338,6 @@ namespace StardewModdingAPI
new Thread(this.RunConsoleLoop).Start();
}
- /// <summary>Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
- /// <param name="mods">The mods to process.</param>
- private ModMetadata[] HandleModDependencies(ModMetadata[] mods)
- {
- this.Monitor.Log("Checking mod dependencies...");
- var unsortedMods = mods.ToList();
- var sortedMods = new Stack<ModMetadata>();
- var visitedMods = new bool[unsortedMods.Count];
- var currentChain = new List<ModMetadata>();
- bool success = true;
-
- for (int index = 0; index < unsortedMods.Count; index++)
- {
- success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
- if (!success)
- break;
- }
-
- if (!success)
- {
- // Failed to sort list, return no mods.
- this.Monitor.Log("No mods will be loaded.", LogLevel.Error);
- return new ModMetadata[0];
- }
-
- return sortedMods.Reverse().ToArray();
- }
-
- /// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
- /// <param name="modIndex">The index of the mod being processed in the <paramref name="unsortedMods"/>.</param>
- /// <param name="visitedMods">The mods which have been processed.</param>
- /// <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>
- /// <param name="unsortedMods">The mods remaining to sort.</param>
- /// <returns>Returns whether the mod can be loaded.</returns>
- private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack<ModMetadata> sortedMods, List<ModMetadata> currentChain, List<ModMetadata> unsortedMods)
- {
- // visit mod
- if (visitedMods[modIndex])
- return true; // already sorted
- ModMetadata mod = unsortedMods[modIndex];
- visitedMods[modIndex] = true;
-
- // process dependencies
- bool success = true;
- if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any())
- {
- // validate required dependencies are present
- {
- string missingMods = null;
- foreach (IManifestDependency dependency in mod.Manifest.Dependencies)
- {
- if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID)))
- missingMods += $"{dependency.UniqueID}, ";
- }
- if (missingMods != null)
- {
- this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error);
- return false;
- }
- }
-
- // get mods which should be loaded before this one
- ModMetadata[] modsToLoadFirst =
- (
- from unsorted in unsortedMods
- where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID)
- select unsorted
- )
- .ToArray();
-
- // detect circular references
- ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains);
- if (circularReferenceMod != null)
- {
- this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error);
- string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}";
- for (int i = currentChain.Count - 1; i >= 0; i--)
- {
- chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain;
- if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break;
- }
- this.Monitor.Log(chain, LogLevel.Error);
- return false;
- }
- currentChain.Add(mod);
-
- // recursively sort dependencies
- foreach (ModMetadata requiredMod in modsToLoadFirst)
- {
- int index = unsortedMods.IndexOf(requiredMod);
- success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
- if (!success)
- break;
- }
- }
-
- // mark mod sorted
- sortedMods.Push(mod);
- currentChain.Remove(mod);
- return success;
- }
-
/// <summary>Run a loop handling console input.</summary>
[SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")]
private void RunConsoleLoop()
@@ -560,147 +457,6 @@ namespace StardewModdingAPI
}
}
- /// <summary>Find all mods in the given folder.</summary>
- /// <param name="rootPath">The root mod path to search.</param>
- /// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param>
- /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
- private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList<Action> deprecationWarnings)
- {
- this.Monitor.Log("Finding mods...");
- void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level);
-
- // load mod metadata
- List<ModMetadata> mods = new List<ModMetadata>();
- foreach (string modRootPath in Directory.GetDirectories(rootPath))
- {
- if (this.Monitor.IsExiting)
- return new ModMetadata[0]; // exit in progress
-
- // init metadata
- string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\');
-
- // passthrough empty directories
- DirectoryInfo directory = new DirectoryInfo(modRootPath);
- while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
- directory = directory.GetDirectories().First();
-
- // get manifest path
- string manifestPath = Path.Combine(directory.FullName, "manifest.json");
- if (!File.Exists(manifestPath))
- {
- LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn);
- continue;
- }
-
- // read manifest
- Manifest manifest;
- try
- {
- // read manifest file
- string json = File.ReadAllText(manifestPath);
- if (string.IsNullOrEmpty(json))
- {
- LogSkip(displayName, "its manifest is empty.");
- continue;
- }
-
- // parse manifest
- manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
- if (manifest == null)
- {
- LogSkip(displayName, "its manifest is invalid.");
- continue;
- }
-
- // validate manifest
- if (string.IsNullOrWhiteSpace(manifest.EntryDll))
- {
- LogSkip(displayName, "its manifest doesn't set an entry DLL.");
- continue;
- }
- if (string.IsNullOrWhiteSpace(manifest.UniqueID))
- deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
- }
- catch (Exception ex)
- {
- LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}");
- continue;
- }
- if (!string.IsNullOrWhiteSpace(manifest.Name))
- displayName = manifest.Name;
-
- // validate compatibility
- ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
- if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
- {
- bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
- bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
-
- string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
- string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
- if (hasOfficialUrl)
- error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
- if (hasUnofficialUrl)
- error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
-
- LogSkip(displayName, error);
- }
-
- // validate SMAPI version
- if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
- {
- try
- {
- ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
- if (minVersion.IsNewerThan(Constants.ApiVersion))
- {
- LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
- continue;
- }
- }
- catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
- {
- LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
- continue;
- }
- }
-
- // create per-save directory
- if (manifest.PerSaveConfigs)
- {
- deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
- try
- {
- string psDir = Path.Combine(directory.FullName, "psconfigs");
- Directory.CreateDirectory(psDir);
- if (!Directory.Exists(psDir))
- {
- LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
- continue;
- }
- }
- catch (Exception ex)
- {
- LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
- continue;
- }
- }
-
- // validate DLL path
- string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
- if (!File.Exists(assemblyPath))
- {
- LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist.");
- continue;
- }
-
- // add mod metadata
- mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility));
- }
-
- return mods.ToArray();
- }
-
/// <summary>Load and hook up the given mods.</summary>
/// <param name="mods">The mods to load.</param>
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 86fc8b2b..2424f438 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -121,6 +121,7 @@
<Compile Include="Events\EventArgsStringChanged.cs" />
<Compile Include="Events\GameEvents.cs" />
<Compile Include="Events\GraphicsEvents.cs" />
+ <Compile Include="Framework\ModLoading\ModResolver.cs" />
<Compile Include="Framework\ModLoading\AssemblyDefinitionResolver.cs" />
<Compile Include="Framework\ModLoading\AssemblyParseResult.cs" />
<Compile Include="Framework\CommandManager.cs" />