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 useful `InputEvents` metadata like the cursor position, grab tile, etc.
* 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.
## 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.");
}
#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

View File

@ -228,13 +228,24 @@ namespace StardewModdingAPI.Framework.ModLoading
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 }
select new
{
ID = entry.UniqueID,
MinVersion = entry.MinimumVersion,
Mod = dependencyMod,
IsRequired =
#if SMAPI_2_0
entry.IsRequired
#else
true
#endif
}
)
.ToArray();
// 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())
{
sortedMods.Push(mod);
@ -248,7 +259,7 @@ namespace StardewModdingAPI.Framework.ModLoading
string[] failedLabels =
(
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)"
)
.ToArray();
@ -265,11 +276,15 @@ namespace StardewModdingAPI.Framework.ModLoading
states[mod] = ModDependencyStatus.Checking;
// recursively sort dependencies
IModMetadata[] modsToLoadFirst = dependencies.Select(p => p.Mod).ToArray();
foreach (IModMetadata requiredMod in modsToLoadFirst)
foreach (var dependency in dependencies)
{
IModMetadata requiredMod = dependency.Mod;
var subchain = new List<IModMetadata>(currentChain) { mod };
// ignore missing optional dependency
if (!dependency.IsRequired && requiredMod == null)
continue;
// detect dependency loop
if (states[requiredMod] == ModDependencyStatus.Checking)
{

View File

@ -12,6 +12,10 @@
/// <summary>The minimum required version (if any).</summary>
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
@ -19,12 +23,20 @@
/// <summary>Construct an instance.</summary>
/// <param name="uniqueID">The unique mod ID to require.</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.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion)
? new SemanticVersion(minimumVersion)
: 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 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));
#endif
}
return result.ToArray();
}

View File

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