diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs
index 72c4692b..7be85a83 100644
--- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs
@@ -36,7 +36,6 @@ namespace StardewModdingAPI.Framework.ModLoading
/// The mod manifest.
/// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.
public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility)
- : this(displayName, directoryPath, manifest, compatibility, ModMetadataStatus.Found, null)
{
this.DisplayName = displayName;
this.DirectoryPath = directoryPath;
@@ -44,21 +43,15 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Compatibility = compatibility;
}
- /// Construct an instance.
- /// The mod's display name.
- /// The mod's full directory path.
- /// The mod manifest.
- /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.
+ /// Set the mod status.
/// The metadata resolution status.
/// The reason the metadata is invalid, if any.
- public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility, ModMetadataStatus status, string error)
+ /// Return the instance for chaining.
+ public ModMetadata SetStatus(ModMetadataStatus status, string error = null)
{
- this.DisplayName = displayName;
- this.DirectoryPath = directoryPath;
- this.Manifest = manifest;
- this.Compatibility = compatibility;
this.Status = status;
this.Error = error;
+ return this;
}
}
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
index 30c38aca..829575af 100644
--- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
@@ -10,99 +10,124 @@ namespace StardewModdingAPI.Framework.ModLoading
/// Finds and processes mod metadata.
internal class ModResolver
{
- /*********
- ** Properties
- *********/
- /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.
- private readonly ModCompatibility[] CompatibilityRecords;
-
-
/*********
** Public methods
*********/
- /// Construct an instance.
- /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.
- public ModResolver(IEnumerable compatibilityRecords)
- {
- this.CompatibilityRecords = compatibilityRecords.ToArray();
- }
-
- /// Read mod metadata from the given folder in dependency order.
+ /// Get manifest metadata for each folder in the given root path.
/// The root path to search for mods.
/// The JSON helper with which to read manifests.
- public IEnumerable GetMods(string rootPath, JsonHelper jsonHelper)
+ /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.
+ /// Returns the manifests by relative folder.
+ public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords)
{
- ModMetadata[] mods = this.GetDataFromFolder(rootPath, jsonHelper).ToArray();
- mods = this.ProcessDependencies(mods.ToArray());
- return mods;
- }
-
-
- /*********
- ** Private methods
- *********/
- /// Find all mods in the given folder.
- /// The root mod path to search.
- /// The JSON helper with which to read the manifest file.
- private IEnumerable GetDataFromFolder(string rootPath, JsonHelper jsonHelper)
- {
- // load mod metadata
+ compatibilityRecords = compatibilityRecords.ToArray();
foreach (DirectoryInfo modDir in this.GetModFolders(rootPath))
{
- string displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\');
-
- // read manifest
- Manifest manifest;
+ // read file
+ Manifest manifest = null;
+ string path = Path.Combine(modDir.FullName, "manifest.json");
+ string error = null;
+ try
{
- string manifestPath = Path.Combine(modDir.FullName, "manifest.json");
- if (!this.TryReadManifest(manifestPath, jsonHelper, out manifest, out string error))
- yield return new ModMetadata(displayName, modDir.FullName, null, null, ModMetadataStatus.Failed, error);
+ // read manifest
+ manifest = jsonHelper.ReadJsonFile(path);
+
+ // validate
+ if (manifest == null)
+ {
+ error = File.Exists(path)
+ ? "its manifest is invalid."
+ : "it doesn't have a manifest.";
+ }
+ else if (string.IsNullOrWhiteSpace(manifest.EntryDll))
+ error = "its manifest doesn't set an entry DLL.";
}
- if (!string.IsNullOrWhiteSpace(manifest.Name))
- displayName = manifest.Name;
-
- // validate compatibility
- ModCompatibility compatibility = this.GetCompatibilityRecord(manifest);
- if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
+ catch (Exception ex)
{
- 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}";
-
- yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, error);
+ error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
}
- // validate SMAPI version
- if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
+ // get compatibility record
+ ModCompatibility compatibility = null;
+ if(manifest != null)
{
- if (!SemanticVersion.TryParse(manifest.MinimumApiVersion, out ISemanticVersion minVersion))
- yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
- if (minVersion.IsNewerThan(Constants.ApiVersion))
- yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
+ string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
+ compatibility = (
+ from mod in compatibilityRecords
+ where
+ mod.ID == key
+ && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
+ && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
+ select mod
+ ).FirstOrDefault();
}
+ // build metadata
+ string displayName = !string.IsNullOrWhiteSpace(manifest?.Name)
+ ? manifest.Name
+ : modDir.FullName.Replace(rootPath, "").Trim('/', '\\');
+ ModMetadataStatus status = error == null
+ ? ModMetadataStatus.Found
+ : ModMetadataStatus.Failed;
- // validate DLL path
- string assemblyPath = Path.Combine(modDir.FullName, manifest.EntryDll);
- if (!File.Exists(assemblyPath))
- {
- yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"its DLL '{manifest.EntryDll}' doesn't exist.");
- continue;
- }
-
- // add mod metadata
- yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility);
+ yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error);
}
}
- /// 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.
+ /// Validate manifest metadata.
+ /// The mod manifests to validate.
+ public void ValidateManifests(IEnumerable mods)
+ {
+ foreach (ModMetadata mod in mods)
+ {
+ // skip if already failed
+ if (mod.Status == ModMetadataStatus.Failed)
+ continue;
+
+ // validate compatibility
+ {
+ ModCompatibility compatibility = mod.Compatibility;
+ if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
+ {
+ bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl);
+ bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.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}";
+
+ mod.SetStatus(ModMetadataStatus.Failed, error);
+ continue;
+ }
+ }
+
+ // validate SMAPI version
+ if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion))
+ {
+ if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion))
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
+ continue;
+ }
+ if (minVersion.IsNewerThan(Constants.ApiVersion))
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
+ continue;
+ }
+ }
+
+ // validate DLL path
+ string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
+ if (!File.Exists(assemblyPath))
+ mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
+ }
+ }
+
+ /// Sort the given mods by the order they should be loaded.
/// The mods to process.
- private ModMetadata[] ProcessDependencies(ModMetadata[] mods)
+ public IEnumerable ProcessDependencies(IEnumerable mods)
{
var unsortedMods = mods.ToList();
var sortedMods = new Stack();
@@ -126,6 +151,10 @@ namespace StardewModdingAPI.Framework.ModLoading
return sortedMods.Reverse().ToArray();
}
+
+ /*********
+ ** Private methods
+ *********/
/// 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.
/// The index of the mod being processed in the .
/// The mods which have been processed.
@@ -215,65 +244,5 @@ namespace StardewModdingAPI.Framework.ModLoading
yield return directory;
}
}
-
- /// Read a manifest file if it's valid, else set a relevant error phrase.
- /// The absolute path to the manifest file.
- /// The JSON helper with which to read the manifest file.
- /// The loaded manifest, if reading succeeded.
- /// The read error, if reading failed.
- /// Returns whether the manifest was read successfully.
- private bool TryReadManifest(string path, JsonHelper jsonHelper, out Manifest manifest, out string errorPhrase)
- {
- try
- {
- // validate path
- if (!File.Exists(path))
- {
- manifest = null;
- errorPhrase = "it doesn't have a manifest.";
- return false;
- }
-
- // parse manifest
- manifest = jsonHelper.ReadJsonFile(path);
- if (manifest == null)
- {
- errorPhrase = "its manifest is invalid.";
- return false;
- }
-
- // validate manifest
- if (string.IsNullOrWhiteSpace(manifest.EntryDll))
- {
- errorPhrase = "its manifest doesn't set an entry DLL.";
- return false;
- }
-
- errorPhrase = null;
- return true;
- }
- catch (Exception ex)
- {
- manifest = null;
- errorPhrase = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
- return false;
- }
- }
-
- /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.
- /// The mod manifest.
- /// Returns the incompatibility record if applicable, else null.
- 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();
- }
}
}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index c8840538..74a9ff8e 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -313,12 +313,12 @@ namespace StardewModdingAPI
// load mods
int modsLoaded;
{
- // get mod metadata (in dependency order)
this.Monitor.Log("Loading mod metadata...");
- JsonHelper jsonHelper = new JsonHelper();
- ModMetadata[] mods = new ModResolver(this.Settings.ModCompatibility)
- .GetMods(Constants.ModPath, new JsonHelper())
- .ToArray();
+ ModResolver resolver = new ModResolver();
+
+ // load manifests
+ ModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray();
+ resolver.ValidateManifests(mods);
// check for deprecated metadata
IList deprecationWarnings = new List();
@@ -326,7 +326,7 @@ namespace StardewModdingAPI
{
// missing unique ID
if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
- deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} doesn't have specify a {nameof(IManifest.UniqueID)} field in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
+ deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} doesn't have specify a {nameof(IManifest.UniqueID)} field in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
// per-save directories
if ((mod.Manifest as Manifest)?.PerSaveConfigs == true)
@@ -350,8 +350,11 @@ namespace StardewModdingAPI
}
}
+ // process dependencies
+ mods = resolver.ProcessDependencies(mods).ToArray();
+
// load mods
- modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings);
+ modsLoaded = this.LoadMods(mods, new JsonHelper(), (SContentManager)Game1.content, deprecationWarnings);
foreach (Action warning in deprecationWarnings)
warning();
}
@@ -515,7 +518,7 @@ namespace StardewModdingAPI
// get basic info
IManifest manifest = metadata.Manifest;
string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll);
-
+
// preprocess & load mod assembly
Assembly modAssembly;
try