summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ModLoading/ModResolver.cs
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-02-24 17:54:31 -0500
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-02-24 17:54:31 -0500
commit414cf5c197b5b59776d3dda914eb15710efb0868 (patch)
tree0393a95194ad78cf4440c68657b0488b7db6d68b /src/SMAPI/Framework/ModLoading/ModResolver.cs
parent5da8b707385b9851ff3f6442de58519125f5c96f (diff)
parentf2e8450706d1971d774f870081deffdb0c6b92eb (diff)
downloadSMAPI-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.cs238
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;
+ }
+ }
}
}