summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--release-notes.md4
-rw-r--r--src/StardewModdingAPI.Tests/Core/ModResolverTests.cs34
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs25
-rw-r--r--src/StardewModdingAPI/Framework/Models/ManifestDependency.cs14
-rw-r--r--src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs5
-rw-r--r--src/StardewModdingAPI/IManifestDependency.cs5
6 files changed, 80 insertions, 7 deletions
diff --git a/release-notes.md b/release-notes.md
index dcc21aaf..ca0e00f7 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -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
diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs
index 36cc3495..b451465e 100644
--- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs
+++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs
@@ -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
diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
index 9c56aaa4..38dddce7 100644
--- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs
@@ -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)
{
diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs
index a0ff0c90..25d92a29 100644
--- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs
+++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs
@@ -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
}
}
}
diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs
index e6d62d50..5be0f0b6 100644
--- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs
+++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs
@@ -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();
}
diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs
index ebb1140e..027c1d59 100644
--- a/src/StardewModdingAPI/IManifestDependency.cs
+++ b/src/StardewModdingAPI/IManifestDependency.cs
@@ -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
}
}