Skip to content

Commit 8fda772

Browse files
committed
Merge branch 'develop' into stable
2 parents 84a12e1 + 9327fb5 commit 8fda772

10 files changed

Lines changed: 78 additions & 34 deletions

File tree

build/common.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed.
77
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
88
<PropertyGroup>
99
<!--set general build properties -->
10-
<Version>4.0.5</Version>
10+
<Version>4.0.6</Version>
1111
<Product>SMAPI</Product>
1212
<LangVersion>latest</LangVersion>
1313
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>

docs/release-notes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
[README](README.md)
22

33
# Release notes
4+
## 4.0.6
5+
Released 07 April 2024 for Stardew Valley 1.6.0 or later.
6+
7+
* For player:
8+
* The SMAPI log file now includes installed mod IDs, to help with troubleshooting (thanks to DecidedlyHuman!).
9+
10+
* For mod authors:
11+
* Added optional [`MinimumGameVersion` manifest field](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Minimum_SMAPI_or_game_version).
12+
413
## 4.0.5
514
Released 06 April 2024 for Stardew Valley 1.6.0 or later.
615

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"Name": "Console Commands",
33
"Author": "SMAPI",
4-
"Version": "4.0.5",
4+
"Version": "4.0.6",
55
"Description": "Adds SMAPI console commands that let you manipulate the game.",
66
"UniqueID": "SMAPI.ConsoleCommands",
77
"EntryDll": "ConsoleCommands.dll",
8-
"MinimumApiVersion": "4.0.5"
8+
"MinimumApiVersion": "4.0.6"
99
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"Name": "Save Backup",
33
"Author": "SMAPI",
4-
"Version": "4.0.5",
4+
"Version": "4.0.6",
55
"Description": "Automatically backs up all your saves once per day into its folder.",
66
"UniqueID": "SMAPI.SaveBackup",
77
"EntryDll": "SaveBackup.dll",
8-
"MinimumApiVersion": "4.0.5"
8+
"MinimumApiVersion": "4.0.6"
99
}

src/SMAPI.Tests/Core/ModResolverTests.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public void ReadBasicManifest_CanReadFile()
8282
[nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}",
8383
[nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll",
8484
[nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}.{Sample.Int()}-{Sample.String()}",
85+
[nameof(IManifest.MinimumGameVersion)] = $"{Sample.Int()}.{Sample.Int()}.{Sample.Int()}-{Sample.String()}",
8586
[nameof(IManifest.Dependencies)] = new[] { originalDependency },
8687
["ExtraString"] = Sample.String(),
8788
["ExtraInt"] = Sample.Int()
@@ -112,7 +113,8 @@ public void ReadBasicManifest_CanReadFile()
112113
Assert.AreEqual(original[nameof(IManifest.Description)], mod.Manifest.Description, "The manifest's description doesn't match.");
113114
Assert.AreEqual(original[nameof(IManifest.EntryDll)], mod.Manifest.EntryDll, "The manifest's entry DLL doesn't match.");
114115
Assert.AreEqual(original[nameof(IManifest.MinimumApiVersion)], mod.Manifest.MinimumApiVersion?.ToString(), "The manifest's minimum API version doesn't match.");
115-
Assert.AreEqual(original[nameof(IManifest.Version)]?.ToString(), mod.Manifest.Version?.ToString(), "The manifest's version doesn't match.");
116+
Assert.AreEqual(original[nameof(IManifest.MinimumGameVersion)], mod.Manifest.MinimumGameVersion?.ToString(), "The manifest's minimum game version doesn't match.");
117+
Assert.AreEqual(original[nameof(IManifest.Version)].ToString(), mod.Manifest.Version.ToString(), "The manifest's version doesn't match.");
116118

117119
Assert.IsNotNull(mod.Manifest.ExtraFields, "The extra fields should not be null.");
118120
Assert.AreEqual(2, mod.Manifest.ExtraFields.Count, "The extra fields should contain two values.");
@@ -133,7 +135,7 @@ public void ReadBasicManifest_CanReadFile()
133135
[Test(Description = "Assert that validation doesn't fail if there are no mods installed.")]
134136
public void ValidateManifests_NoMods_DoesNothing()
135137
{
136-
new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
138+
new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
137139
}
138140

139141
[Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")]
@@ -144,7 +146,7 @@ public void ValidateManifests_Skips_Failed()
144146
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
145147

146148
// act
147-
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
149+
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
148150

149151
// assert
150152
mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status.");
@@ -161,7 +163,7 @@ public void ValidateManifests_ModStatus_AssumeBroken_Fails()
161163
});
162164

163165
// act
164-
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
166+
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
165167

166168
// assert
167169
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -175,7 +177,21 @@ public void ValidateManifests_MinimumApiVersion_Fails()
175177
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
176178

177179
// act
178-
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
180+
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
181+
182+
// assert
183+
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
184+
}
185+
186+
[Test(Description = "Assert that validation fails when the minimum game version is higher than the current Stardew Valley version.")]
187+
public void ValidateManifests_MinimumGameVersion_Fails()
188+
{
189+
// arrange
190+
Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
191+
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumGameVersion: "1.6.9"));
192+
193+
// act
194+
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
179195

180196
// assert
181197
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -190,7 +206,7 @@ public void ValidateManifests_MissingEntryDLL_Fails()
190206
Directory.CreateDirectory(directoryPath);
191207

192208
// act
193-
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup);
209+
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup);
194210

195211
// assert
196212
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -207,7 +223,7 @@ public void ValidateManifests_DuplicateUniqueID_Fails()
207223
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
208224

209225
// act
210-
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
226+
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup, validateFilesExist: false);
211227

212228
// assert
213229
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the first mod with a unique ID.");
@@ -233,7 +249,7 @@ public void ValidateManifests_Valid_Passes()
233249
mock.Setup(p => p.DirectoryPath).Returns(modFolder);
234250

235251
// act
236-
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup);
252+
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0.0"), gameVersion: new SemanticVersion("1.0.0"), getUpdateUrl: _ => null, getFileLookup: this.GetFileLookup);
237253

238254
// assert
239255
// if Moq doesn't throw a method-not-setup exception, the validation didn't override the status.
@@ -497,8 +513,9 @@ private IFileLookup GetFileLookup(string rootDirectory)
497513
/// <param name="entryDll">The <see cref="IManifest.EntryDll"/> value, or <c>null</c> for a generated value.</param>
498514
/// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param>
499515
/// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param>
516+
/// <param name="minimumGameVersion">The <see cref="IManifest.MinimumGameVersion"/> value.</param>
500517
/// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param>
501-
private Manifest GetManifest(string? id = null, string? name = null, string? version = null, string? entryDll = null, string? contentPackForID = null, string? minimumApiVersion = null, IManifestDependency[]? dependencies = null)
518+
private Manifest GetManifest(string? id = null, string? name = null, string? version = null, string? entryDll = null, string? contentPackForID = null, string? minimumApiVersion = null, string? minimumGameVersion = null, IManifestDependency[]? dependencies = null)
502519
{
503520
return new Manifest(
504521
uniqueId: id ?? $"{Sample.String()}.{Sample.String()}",
@@ -509,6 +526,7 @@ private Manifest GetManifest(string? id = null, string? name = null, string? ver
509526
entryDll: entryDll ?? $"{Sample.String()}.dll",
510527
contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID, null) : null,
511528
minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null,
529+
minimumGameVersion: minimumGameVersion != null ? new SemanticVersion(minimumGameVersion) : null,
512530
dependencies: dependencies ?? Array.Empty<IManifestDependency>(),
513531
updateKeys: Array.Empty<string>()
514532
);

src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public interface IManifest
2323
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
2424
ISemanticVersion? MinimumApiVersion { get; }
2525

26+
/// <summary>The minimum Stardew Valley version required by this mod, if any.</summary>
27+
ISemanticVersion? MinimumGameVersion { get; }
28+
2629
/// <summary>The unique mod ID.</summary>
2730
string UniqueID { get; }
2831

src/SMAPI.Toolkit/Serialization/Models/Manifest.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,48 @@
77

88
namespace StardewModdingAPI.Toolkit.Serialization.Models
99
{
10-
/// <summary>A manifest which describes a mod for SMAPI.</summary>
10+
/// <inheritdoc cref="IManifest" />
1111
public class Manifest : IManifest
1212
{
1313
/*********
1414
** Accessors
1515
*********/
16-
/// <summary>The mod name.</summary>
16+
/// <inheritdoc />
1717
public string Name { get; }
1818

19-
/// <summary>A brief description of the mod.</summary>
19+
/// <inheritdoc />
2020
public string Description { get; }
2121

22-
/// <summary>The mod author's name.</summary>
22+
/// <inheritdoc />
2323
public string Author { get; }
2424

25-
/// <summary>The mod version.</summary>
25+
/// <inheritdoc />
2626
public ISemanticVersion Version { get; }
2727

28-
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
28+
/// <inheritdoc />
2929
public ISemanticVersion? MinimumApiVersion { get; }
3030

31-
/// <summary>The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary>
31+
/// <inheritdoc />
32+
public ISemanticVersion? MinimumGameVersion { get; }
33+
34+
/// <inheritdoc />
3235
public string? EntryDll { get; }
3336

34-
/// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="Manifest.EntryDll"/>.</summary>
37+
/// <inheritdoc />
3538
[JsonConverter(typeof(ManifestContentPackForConverter))]
3639
public IManifestContentPackFor? ContentPackFor { get; }
3740

38-
/// <summary>The other mods that must be loaded before this mod.</summary>
41+
/// <inheritdoc />
3942
[JsonConverter(typeof(ManifestDependencyArrayConverter))]
4043
public IManifestDependency[] Dependencies { get; }
4144

42-
/// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
45+
/// <inheritdoc />
4346
public string[] UpdateKeys { get; private set; }
4447

45-
/// <summary>The unique mod ID.</summary>
48+
/// <inheritdoc />
4649
public string UniqueID { get; }
4750

48-
/// <summary>Any manifest fields which didn't match a valid field.</summary>
51+
/// <inheritdoc />
4952
[JsonExtensionData]
5053
public IDictionary<string, object> ExtraFields { get; } = new Dictionary<string, object>();
5154

@@ -68,6 +71,7 @@ public Manifest(string uniqueID, string name, string author, string description,
6871
description: description,
6972
version: version,
7073
minimumApiVersion: null,
74+
minimumGameVersion: null,
7175
entryDll: null,
7276
contentPackFor: contentPackFor != null
7377
? new ManifestContentPackFor(contentPackFor, null)
@@ -84,19 +88,21 @@ public Manifest(string uniqueID, string name, string author, string description,
8488
/// <param name="description">A brief description of the mod.</param>
8589
/// <param name="version">The mod version.</param>
8690
/// <param name="minimumApiVersion">The minimum SMAPI version required by this mod, if any.</param>
91+
/// <param name="minimumGameVersion">The minimum Stardew Valley version required by this mod, if any.</param>
8792
/// <param name="entryDll">The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</param>
8893
/// <param name="contentPackFor">The modID which will read this as a content pack.</param>
8994
/// <param name="dependencies">The other mods that must be loaded before this mod.</param>
9095
/// <param name="updateKeys">The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</param>
9196
[JsonConstructor]
92-
public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys)
97+
public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, ISemanticVersion? minimumGameVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys)
9398
{
9499
this.UniqueID = this.NormalizeField(uniqueId);
95100
this.Name = this.NormalizeField(name, replaceSquareBrackets: true);
96101
this.Author = this.NormalizeField(author);
97102
this.Description = this.NormalizeField(description);
98103
this.Version = version;
99104
this.MinimumApiVersion = minimumApiVersion;
105+
this.MinimumGameVersion = minimumGameVersion;
100106
this.EntryDll = this.NormalizeField(entryDll);
101107
this.ContentPackFor = contentPackFor;
102108
this.Dependencies = dependencies ?? Array.Empty<IManifestDependency>();

src/SMAPI/Constants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ internal static class EarlyConstants
4949
internal static int? LogScreenId { get; set; }
5050

5151
/// <summary>SMAPI's current raw semantic version.</summary>
52-
internal static string RawApiVersion = "4.0.5";
52+
internal static string RawApiVersion = "4.0.6";
5353
}
5454

5555
/// <summary>Contains SMAPI's constants and assumptions.</summary>

src/SMAPI/Framework/ModLoading/ModResolver.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ public IEnumerable<IModMetadata> ReadManifests(ModToolkit toolkit, string rootPa
6666
/// <summary>Validate manifest metadata.</summary>
6767
/// <param name="mods">The mod manifests to validate.</param>
6868
/// <param name="apiVersion">The current SMAPI version.</param>
69+
/// <param name="gameVersion">The current Stardew Valley version.</param>
6970
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
7071
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
7172
/// <param name="validateFilesExist">Whether to validate that files referenced in the manifest (like <see cref="IManifest.EntryDll"/>) exist on disk. This can be disabled to only validate the manifest itself.</param>
7273
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifest values may be null before they're validated.")]
7374
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "Manifest values may be null before they're validated.")]
74-
public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl, Func<string, IFileLookup> getFileLookup, bool validateFilesExist = true)
75+
public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Func<string, string?> getUpdateUrl, Func<string, IFileLookup> getFileLookup, bool validateFilesExist = true)
7576
{
7677
mods = mods.ToArray();
7778

@@ -126,6 +127,13 @@ public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion a
126127
continue;
127128
}
128129

130+
// validate game version
131+
if (mod.Manifest.MinimumGameVersion?.IsNewerThan(gameVersion) == true)
132+
{
133+
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, $"it needs Stardew Valley {mod.Manifest.MinimumGameVersion} or later. Please update your game to the latest version to use this mod.");
134+
continue;
135+
}
136+
129137
// validate manifest format
130138
if (!ManifestValidator.TryValidateFields(mod.Manifest, out string manifestError))
131139
{

0 commit comments

Comments
 (0)