add optional mod dependencies in SMAPI 2.0 (#287)

This commit is contained in:
Jesse Plamondon-Willard 2017-07-06 17:46:04 -04:00
parent e2b9a4bab3
commit d928bf188e
6 changed files with 80 additions and 7 deletions

View File

@ -11,7 +11,9 @@ For mod developers:
* Added `InputEvents` which unify keyboard, mouse, and controller input for much simpler input handling (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Input_events)). * Added `InputEvents` which unify keyboard, mouse, and controller input for much simpler input handling (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Input_events)).
* Added useful `InputEvents` metadata like the cursor position, grab tile, etc. * Added useful `InputEvents` metadata like the cursor position, grab tile, etc.
* Added ability to prevent the game from handling a button press via `InputEvents`. * Added ability to prevent the game from handling a button press via `InputEvents`.
* The `manifest.json` version can now be specified as a string. * In `manifest.json`:
* Dependencies can now be optional.
* The version can now be a string like `"1.0-alpha"` instead of a structure.
* Removed all deprecated code. * Removed all deprecated code.
## 1.15 ## 1.15

View File

@ -411,6 +411,40 @@ namespace StardewModdingAPI.Tests.Core
Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A.");
} }
#if SMAPI_2_0
[Test(Description = "Assert that optional dependencies are sorted correctly if present.")]
public void ProcessDependencies_IfOptional()
{
// arrange
// A ◀── B
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false);
// act
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }).ToArray();
// assert
Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input.");
Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B.");
Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A.");
}
[Test(Description = "Assert that optional dependencies are accepted if they're missing.")]
public void ProcessDependencies_IfOptional_SucceedsIfMissing()
{
// arrange
// A ◀── B where A doesn't exist
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false);
// act
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }).ToArray();
// assert
Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input.");
Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod.");
}
#endif
/********* /*********
** Private methods ** Private methods

View File

@ -228,13 +228,24 @@ namespace StardewModdingAPI.Framework.ModLoading
from entry in mod.Manifest.Dependencies from entry in mod.Manifest.Dependencies
let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase))
orderby entry.UniqueID orderby entry.UniqueID
select new { ID = entry.UniqueID, MinVersion = entry.MinimumVersion, Mod = dependencyMod } select new
{
ID = entry.UniqueID,
MinVersion = entry.MinimumVersion,
Mod = dependencyMod,
IsRequired =
#if SMAPI_2_0
entry.IsRequired
#else
true
#endif
}
) )
.ToArray(); .ToArray();
// missing required dependencies, mark failed // missing required dependencies, mark failed
{ {
string[] failedIDs = (from entry in dependencies where entry.Mod == null select entry.ID).ToArray(); string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray();
if (failedIDs.Any()) if (failedIDs.Any())
{ {
sortedMods.Push(mod); sortedMods.Push(mod);
@ -248,7 +259,7 @@ namespace StardewModdingAPI.Framework.ModLoading
string[] failedLabels = string[] failedLabels =
( (
from entry in dependencies from entry in dependencies
where entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version)
select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)"
) )
.ToArray(); .ToArray();
@ -265,11 +276,15 @@ namespace StardewModdingAPI.Framework.ModLoading
states[mod] = ModDependencyStatus.Checking; states[mod] = ModDependencyStatus.Checking;
// recursively sort dependencies // recursively sort dependencies
IModMetadata[] modsToLoadFirst = dependencies.Select(p => p.Mod).ToArray(); foreach (var dependency in dependencies)
foreach (IModMetadata requiredMod in modsToLoadFirst)
{ {
IModMetadata requiredMod = dependency.Mod;
var subchain = new List<IModMetadata>(currentChain) { mod }; var subchain = new List<IModMetadata>(currentChain) { mod };
// ignore missing optional dependency
if (!dependency.IsRequired && requiredMod == null)
continue;
// detect dependency loop // detect dependency loop
if (states[requiredMod] == ModDependencyStatus.Checking) if (states[requiredMod] == ModDependencyStatus.Checking)
{ {

View File

@ -12,6 +12,10 @@
/// <summary>The minimum required version (if any).</summary> /// <summary>The minimum required version (if any).</summary>
public ISemanticVersion MinimumVersion { get; set; } public ISemanticVersion MinimumVersion { get; set; }
#if SMAPI_2_0
/// <summary>Whether the dependency must be installed to use the mod.</summary>
public bool IsRequired { get; set; }
#endif
/********* /*********
** Public methods ** Public methods
@ -19,12 +23,20 @@
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="uniqueID">The unique mod ID to require.</param> /// <param name="uniqueID">The unique mod ID to require.</param>
/// <param name="minimumVersion">The minimum required version (if any).</param> /// <param name="minimumVersion">The minimum required version (if any).</param>
public ManifestDependency(string uniqueID, string minimumVersion) /// <param name="required">Whether the dependency must be installed to use the mod.</param>
public ManifestDependency(string uniqueID, string minimumVersion
#if SMAPI_2_0
, bool required = true
#endif
)
{ {
this.UniqueID = uniqueID; this.UniqueID = uniqueID;
this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion)
? new SemanticVersion(minimumVersion) ? new SemanticVersion(minimumVersion)
: null; : null;
#if SMAPI_2_0
this.IsRequired = required;
#endif
} }
} }
} }

View File

@ -73,7 +73,12 @@ namespace StardewModdingAPI.Framework.Serialisation
{ {
string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID)); string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID));
string minVersion = obj.Value<string>(nameof(IManifestDependency.MinimumVersion)); string minVersion = obj.Value<string>(nameof(IManifestDependency.MinimumVersion));
#if SMAPI_2_0
bool required = obj.Value<bool?>(nameof(IManifestDependency.IsRequired)) ?? true;
result.Add(new ManifestDependency(uniqueID, minVersion, required));
#else
result.Add(new ManifestDependency(uniqueID, minVersion)); result.Add(new ManifestDependency(uniqueID, minVersion));
#endif
} }
return result.ToArray(); return result.ToArray();
} }

View File

@ -11,5 +11,10 @@
/// <summary>The minimum required version (if any).</summary> /// <summary>The minimum required version (if any).</summary>
ISemanticVersion MinimumVersion { get; } ISemanticVersion MinimumVersion { get; }
#if SMAPI_2_0
/// <summary>Whether the dependency must be installed to use the mod.</summary>
bool IsRequired { get; }
#endif
} }
} }