add optional mod dependencies in SMAPI 2.0 (#287)
This commit is contained in:
parent
e2b9a4bab3
commit
d928bf188e
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue