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