diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs | 15 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 219 | ||||
-rw-r--r-- | src/StardewModdingAPI/Program.cs | 19 |
3 files changed, 109 insertions, 144 deletions
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 /// <param name="manifest">The mod manifest.</param> /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> 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; } - /// <summary>Construct an instance.</summary> - /// <param name="displayName">The mod's display name.</param> - /// <param name="directoryPath">The mod's full directory path.</param> - /// <param name="manifest">The mod manifest.</param> - /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> + /// <summary>Set the mod status.</summary> /// <param name="status">The metadata resolution status.</param> /// <param name="error">The reason the metadata is invalid, if any.</param> - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility, ModMetadataStatus status, string error) + /// <returns>Return the instance for chaining.</returns> + 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 @@ -11,98 +11,123 @@ namespace StardewModdingAPI.Framework.ModLoading internal class ModResolver { /********* - ** Properties - *********/ - /// <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 ModResolver(IEnumerable<ModCompatibility> compatibilityRecords) - { - this.CompatibilityRecords = compatibilityRecords.ToArray(); - } - - /// <summary>Read mod metadata from the given folder in dependency order.</summary> + /// <summary>Get manifest metadata for each folder in the given root path.</summary> /// <param name="rootPath">The root path to search for mods.</param> /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> - public IEnumerable<ModMetadata> GetMods(string rootPath, JsonHelper jsonHelper) - { - ModMetadata[] mods = this.GetDataFromFolder(rootPath, jsonHelper).ToArray(); - mods = this.ProcessDependencies(mods.ToArray()); - return mods; - } - - - /********* - ** Private methods - *********/ - /// <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> - private IEnumerable<ModMetadata> GetDataFromFolder(string rootPath, JsonHelper jsonHelper) + /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> + /// <returns>Returns the manifests by relative folder.</returns> + public IEnumerable<ModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModCompatibility> compatibilityRecords) { - // load mod metadata + compatibilityRecords = compatibilityRecords.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { - string displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + // read file + Manifest manifest = null; + string path = Path.Combine(modDir.FullName, "manifest.json"); + string error = null; + try + { + // read manifest + manifest = jsonHelper.ReadJsonFile<Manifest>(path); - // read manifest - Manifest manifest; + // 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."; + } + catch (Exception ex) { - 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); + error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - if (!string.IsNullOrWhiteSpace(manifest.Name)) - displayName = manifest.Name; - // validate compatibility - ModCompatibility compatibility = this.GetCompatibilityRecord(manifest); - if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) + // get compatibility record + ModCompatibility compatibility = null; + if(manifest != null) { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); + 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; + + yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error); + } + } - 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}"; + /// <summary>Validate manifest metadata.</summary> + /// <param name="mods">The mod manifests to validate.</param> + public void ValidateManifests(IEnumerable<ModMetadata> mods) + { + foreach (ModMetadata mod in mods) + { + // skip if already failed + if (mod.Status == ModMetadataStatus.Failed) + continue; - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, error); + // 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(manifest.MinimumApiVersion)) + if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion)) { - 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 (!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)) - 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."); + { + 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(modDir.FullName, manifest.EntryDll); + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.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); + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); } } - /// <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> + /// <summary>Sort the given mods by the order they should be loaded.</summary> /// <param name="mods">The mods to process.</param> - private ModMetadata[] ProcessDependencies(ModMetadata[] mods) + public IEnumerable<ModMetadata> ProcessDependencies(IEnumerable<ModMetadata> mods) { var unsortedMods = mods.ToList(); var sortedMods = new Stack<ModMetadata>(); @@ -126,6 +151,10 @@ namespace StardewModdingAPI.Framework.ModLoading return sortedMods.Reverse().ToArray(); } + + /********* + ** Private methods + *********/ /// <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> @@ -215,65 +244,5 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } - - /// <summary>Read a manifest file if it's valid, else set a relevant error phrase.</summary> - /// <param name="path">The absolute path to the manifest file.</param> - /// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param> - /// <param name="manifest">The loaded manifest, if reading succeeded.</param> - /// <param name="errorPhrase">The read error, if reading failed.</param> - /// <returns>Returns whether the manifest was read successfully.</returns> - 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<Manifest>(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; - } - } - - /// <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(); - } } } 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<Action> deprecationWarnings = new List<Action>(); @@ -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 |