diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-24 17:54:31 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-24 17:54:31 -0500 |
commit | 414cf5c197b5b59776d3dda914eb15710efb0868 (patch) | |
tree | 0393a95194ad78cf4440c68657b0488b7db6d68b /src/SMAPI/Framework/ModLoading/ModResolver.cs | |
parent | 5da8b707385b9851ff3f6442de58519125f5c96f (diff) | |
parent | f2e8450706d1971d774f870081deffdb0c6b92eb (diff) | |
download | SMAPI-414cf5c197b5b59776d3dda914eb15710efb0868.tar.gz SMAPI-414cf5c197b5b59776d3dda914eb15710efb0868.tar.bz2 SMAPI-414cf5c197b5b59776d3dda914eb15710efb0868.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ModLoading/ModResolver.cs')
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModResolver.cs | 238 |
1 files changed, 156 insertions, 82 deletions
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9802d9e9..ba6dab1a 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.ModLoading { @@ -17,12 +19,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// <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> - /// <param name="dataRecords">Metadata about mods from SMAPI's internal data.</param> + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <returns>Returns the manifests by relative folder.</returns> - public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModDataRecord> dataRecords) + public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) { - dataRecords = dataRecords.ToArray(); - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file @@ -31,18 +31,13 @@ namespace StardewModdingAPI.Framework.ModLoading string error = null; try { - // read manifest manifest = jsonHelper.ReadJsonFile<Manifest>(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."; } catch (SParseException ex) { @@ -53,26 +48,27 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get internal data record (if any) - ModDataRecord dataRecord = null; - if (manifest != null) + // parse internal data record (if any) + ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + + // get display name + string displayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = dataRecord?.DisplayName; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); + + // apply defaults + if (manifest != null && dataRecord != null) { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); + if (dataRecord.UpdateKey != null) + manifest.UpdateKeys = new[] { dataRecord.UpdateKey }; } - // add default update keys - if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) - manifest.UpdateKeys = dataRecord.UpdateKeys; - // 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, dataRecord).SetStatus(status, error); } } @@ -80,8 +76,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> - /// <param name="vendorModUrls">Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID).</param> - public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, IDictionary<string, string> vendorModUrls) + /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> + public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string> getUpdateUrl) { mods = mods.ToArray(); @@ -92,42 +88,35 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; - // validate compatibility - ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); - switch (compatibility?.Status) + // validate compatibility from internal data + switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List<string> updateUrls = new List<string>(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { - string[] parts = key.Split(new[] { ':' }, 2); - if (parts.Length != 2) - continue; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - updateUrls.Add(string.Format(urlTemplate, modID)); + string url = getUpdateUrl(key); + if (url != null) + updateUrls.Add(url); } if (mod.DataRecord.AlternativeUrl != null) updateUrls.Add(mod.DataRecord.AlternativeUrl); // build error string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) error += "newer version"; else - error += $"version newer than {compatibility.UpperVersion}"; + error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); @@ -142,24 +131,52 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL value - if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll)) - { - mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field."); - continue; - } - if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + // validate DLL / content pack fields { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } + bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); + bool isContentPack = mod.Manifest.ContentPackFor != null; - // 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."); - continue; + // validate field presence + if (!hasDll && !isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + continue; + } + if (hasDll && isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + continue; + } + + // validate DLL + if (hasDll) + { + // invalid filename format + if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + continue; + } + + // invalid 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."); + continue; + } + } + + // validate content pack + else + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + continue; + } + } } // validate required fields @@ -197,7 +214,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Sort the given mods by the order they should be loaded.</summary> /// <param name="mods">The mods to process.</param> - public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods) + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> + public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods, ModDatabase modDatabase) { // initialise metadata mods = mods.ToArray(); @@ -213,7 +231,7 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List<IModMetadata>()); + this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List<IModMetadata>()); return sortedMods.Reverse(); } @@ -224,12 +242,13 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// <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="mods">The full list of mods being validated.</param> + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <param name="mod">The mod whose dependencies to process.</param> /// <param name="states">The dependency state for each mod.</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> /// <returns>Returns the mod dependency status.</returns> - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain) + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain) { // check if already visited switch (states[mod]) @@ -255,36 +274,32 @@ namespace StardewModdingAPI.Framework.ModLoading throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } - // no dependencies, mark sorted - if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) + // collect dependencies + ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + + // mark sorted if no dependencies + if (!dependencies.Any()) { sortedMods.Push(mod); return states[mod] = ModDependencyStatus.Sorted; } - // get dependencies - var dependencies = - ( - from entry in mod.Manifest.Dependencies - let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - orderby entry.UniqueID - select new - { - ID = entry.UniqueID, - MinVersion = entry.MinimumVersion, - Mod = dependencyMod, - IsRequired = entry.IsRequired - } - ) - .ToArray(); - - // missing required dependencies, mark failed + // mark failed if missing dependencies { - string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); - if (failedIDs.Any()) + string[] failedModNames = ( + from entry in dependencies + where entry.IsRequired && entry.Mod == null + let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + let modUrl = modDatabase.GetModPageUrlFor(entry.ID) + orderby displayName + select modUrl != null + ? $"{displayName}: {modUrl}" + : displayName + ).ToArray(); + if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -329,7 +344,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // recursively process each dependency - var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); + var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); switch (substatus) { // sorted successfully @@ -374,5 +389,64 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } + + /// <summary>Get the dependencies declared in a manifest.</summary> + /// <param name="manifest">The mod manifest.</param> + /// <param name="loadedMods">The loaded mods.</param> + private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + { + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + + // yield dependencies + if (manifest.Dependencies != null) + { + foreach (var entry in manifest.Dependencies) + yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired); + } + + // yield content pack parent + if (manifest.ContentPackFor != null) + yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true); + } + + + /********* + ** Private models + *********/ + /// <summary>Represents a dependency from one mod to another.</summary> + private struct ModDependency + { + /********* + ** Accessors + *********/ + /// <summary>The unique ID of the required mod.</summary> + public string ID { get; } + + /// <summary>The minimum required version (if any).</summary> + public ISemanticVersion MinVersion { get; } + + /// <summary>Whether the mod shouldn't be loaded if the dependency isn't available.</summary> + public bool IsRequired { get; } + + /// <summary>The loaded mod that fulfills the dependency (if available).</summary> + public IModMetadata Mod { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The unique ID of the required mod.</param> + /// <param name="minVersion">The minimum required version (if any).</param> + /// <param name="mod">The loaded mod that fulfills the dependency (if available).</param> + /// <param name="isRequired">Whether the mod shouldn't be loaded if the dependency isn't available.</param> + public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired) + { + this.ID = id; + this.MinVersion = minVersion; + this.Mod = mod; + this.IsRequired = isRequired; + } + } } } |