From 024489c33827ce8e1463eac199daa996a8a99216 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 18:50:46 -0500 Subject: overhaul internal mod data format (#439) The new format is much more concise, reduces the memory footprint by only parsing metadata for loaded mods, and adds support for versioning and defaulting most fields. --- src/SMAPI/Framework/Models/ModCompatibility.cs | 55 ------ src/SMAPI/Framework/Models/ModDataField.cs | 82 ++++++++ src/SMAPI/Framework/Models/ModDataFieldKey.cs | 18 ++ src/SMAPI/Framework/Models/ModDataID.cs | 85 --------- src/SMAPI/Framework/Models/ModDataRecord.cs | 220 ++++++++++++++++++++-- src/SMAPI/Framework/Models/ParsedModDataRecord.cs | 48 +++++ 6 files changed, 348 insertions(+), 160 deletions(-) delete mode 100644 src/SMAPI/Framework/Models/ModCompatibility.cs create mode 100644 src/SMAPI/Framework/Models/ModDataField.cs create mode 100644 src/SMAPI/Framework/Models/ModDataFieldKey.cs delete mode 100644 src/SMAPI/Framework/Models/ModDataID.cs create mode 100644 src/SMAPI/Framework/Models/ParsedModDataRecord.cs (limited to 'src/SMAPI/Framework/Models') diff --git a/src/SMAPI/Framework/Models/ModCompatibility.cs b/src/SMAPI/Framework/Models/ModCompatibility.cs deleted file mode 100644 index 54737e6c..00000000 --- a/src/SMAPI/Framework/Models/ModCompatibility.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Models -{ - /// Specifies the compatibility of a given mod version range. - internal class ModCompatibility - { - /********* - ** Accessors - *********/ - /// The lowest version in the range, or null for all past versions. - public ISemanticVersion LowerVersion { get; } - - /// The highest version in the range, or null for all future versions. - public ISemanticVersion UpperVersion { get; } - - /// The mod compatibility. - public ModStatus Status { get; } - - /// The reason phrase to show in log output, or null to use the default value. - /// For example, "this version is incompatible with the latest version of the game". - public string ReasonPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range. - /// The mod compatibility. - /// The reason phrase to show in log output, or null to use the default value. - public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) - { - // extract version strings - string[] versions = versionRange.Split('~'); - if (versions.Length != 2) - throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); - - // initialise - this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; - this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; - this.Status = status; - this.ReasonPhrase = reasonPhrase; - } - - /// Get whether a given version is contained within this compatibility range. - /// The version to check. - public bool MatchesVersion(ISemanticVersion version) - { - return - (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataField.cs b/src/SMAPI/Framework/Models/ModDataField.cs new file mode 100644 index 00000000..0812b39b --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Framework.Models +{ + /// A versioned mod metadata field. + internal class ModDataField + { + /********* + ** Accessors + *********/ + /// The field key. + public ModDataFieldKey Key { get; } + + /// The field value. + public string Value { get; } + + /// Whether this field should only be applied if it's not already set. + public bool IsDefault { get; } + + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field key. + /// The field value. + /// Whether this field should only be applied if it's not already set. + /// The lowest version in the range, or null for all past versions. + /// The highest version in the range, or null for all future versions. + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// Get whether this data field applies for the given manifest. + /// The mod manifest. + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// Get whether a manifest field has a meaningful value for the purposes of enforcing . + /// The mod manifest. + /// The field key matching . + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI/Framework/Models/ModDataFieldKey.cs b/src/SMAPI/Framework/Models/ModDataFieldKey.cs new file mode 100644 index 00000000..5767afc9 --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// The valid field keys. + public enum ModDataFieldKey + { + /// A manifest update key. + UpdateKey, + + /// An alternative URL the player can check for an updated version. + AlternativeUrl, + + /// The mod's predefined compatibility status. + Status, + + /// A reason phrase for the , or null to use the default reason. + StatusReasonPhrase + } +} diff --git a/src/SMAPI/Framework/Models/ModDataID.cs b/src/SMAPI/Framework/Models/ModDataID.cs deleted file mode 100644 index d19434fa..00000000 --- a/src/SMAPI/Framework/Models/ModDataID.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// Uniquely identifies a mod in SMAPI's internal data. - /// - /// This represents a custom format which uniquely identifies a mod across all versions, even - /// if its field values change or it doesn't specify a unique ID. This is mapped to a string - /// with the following format: - /// - /// 1. If the mod's identifier changed over time, multiple variants can be separated by the | - /// character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. - /// - internal class ModDataID - { - /********* - ** Properties - *********/ - /// The unique sets of field values which identify this mod. - private readonly FieldSnapshot[] Snapshots; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ModDataID() { } - - /// Construct an instance. - /// The mod identifier string (see remarks on ). - public ModDataID(string data) - { - this.Snapshots = - ( - from string part in data.Split('|') - let str = part.Trim() - select str.StartsWith("{") - ? JsonConvert.DeserializeObject(str) - : new FieldSnapshot { ID = str } - ) - .ToArray(); - } - - /// Get whether this ID matches a given mod manifest. - /// The mod's unique ID, or a substitute ID if it isn't set in the manifest. - /// The manifest to check. - public bool Matches(string id, IManifest manifest) - { - return this.Snapshots.Any(snapshot => - snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) - && ( - snapshot.Author == null - || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) - ); - } - - - /********* - ** Private models - *********/ - /// A unique set of fields which identifies the mod. - private class FieldSnapshot - { - /********* - ** Accessors - *********/ - /// The unique mod ID. - public string ID { get; set; } - - /// The mod name, or null to ignore the mod name. - public string Name { get; set; } - - /// The author name, or null to ignore the author. - public string Author { get; set; } - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs index 580acb70..2c26741c 100644 --- a/src/SMAPI/Framework/Models/ModDataRecord.cs +++ b/src/SMAPI/Framework/Models/ModDataRecord.cs @@ -1,49 +1,188 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.Serialization; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; +using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Framework.Models { - /// Metadata about a mod from SMAPI's internal data. + /// Raw mod metadata from SMAPI's internal mod list. internal class ModDataRecord { /********* - ** Accessors + ** Properties *********/ - /// The unique mod identifier. - [JsonConverter(typeof(ModDataIdConverter))] - public ModDataID ID { get; set; } + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + private IDictionary ExtensionData; - /// A value to inject into field if it's not already set. - public string[] UpdateKeys { get; set; } - /// The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible. - public string AlternativeUrl { get; set; } + /********* + ** Accessors + *********/ + /// The mod's current unique ID. + public string ID { get; set; } - /// The compatibility of given mod versions (if any). - [JsonConverter(typeof(ModCompatibilityArrayConverter))] - public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; + /// The former mod IDs (if any). + /// + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// | character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// + public string FormerIDs { get; set; } - /// Map local versions to a semantic version for update checks. + /// Maps local versions to a semantic version for update checks. public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - /// Map remote versions to a semantic version for update checks. + /// Maps remote versions to a semantic version for update checks. public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + /// The versioned field data. + /// + /// This maps field names to values. This should be accessed via . + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and Default, separated by pipes (whitespace trimmed). For example, Name + /// will always override the name, Default | Name will only override a blank + /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. + /// - The version format is min~max (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a value. + /// + public IDictionary Fields { get; set; } = new Dictionary(); + /********* ** Public methods *********/ - /// Get the compatibility record for a given version, if any. - /// The mod version to check. - public ModCompatibility GetCompatibility(ISemanticVersion version) + /// Get whether the manifest matches the field. + /// The mod manifest to check. + public bool Matches(IManifest manifest) + { + // try main ID + if (this.ID != null && this.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (this.FormerIDs != null) + { + foreach (string part in this.FormerIDs.Split('|')) + { + // packed field snapshot + if (part.StartsWith("{")) + { + FieldSnapshot snapshot = JsonConvert.DeserializeObject(part); + bool isMatch = + (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase)) + && ( + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + + if (isMatch) + return true; + } + + // plain ID + else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + + // no match + return false; + } + + /// Get a parsed representation of the . + public IEnumerable GetFields() { - return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); + foreach (KeyValuePair pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ParsedModDataRecord ParseFieldsFor(IManifest manifest) + { + ParsedModDataRecord parsed = new ParsedModDataRecord { DataRecord = this }; + foreach (ModDataField field in this.GetFields().Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; } /// Get a semantic local version for update checks. - /// The local version to normalise. + /// The remote version to normalise. public string GetLocalVersionForUpdateChecks(string version) { return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) @@ -59,5 +198,46 @@ namespace StardewModdingAPI.Framework.Models ? newVersion : version; } + + + /********* + ** Private methods + *********/ + /// The method invoked after JSON deserialisation. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + + + /********* + ** Private models + *********/ + /// A unique set of fields which identifies the mod. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// The unique mod ID (or null to ignore it). + public string ID { get; set; } + + /// The entry DLL (or null to ignore it). + public string EntryDll { get; set; } + + /// The mod name (or null to ignore it). + public string Name { get; set; } + + /// The author name (or null to ignore it). + public string Author { get; set; } + } } } diff --git a/src/SMAPI/Framework/Models/ParsedModDataRecord.cs b/src/SMAPI/Framework/Models/ParsedModDataRecord.cs new file mode 100644 index 00000000..0abc7b89 --- /dev/null +++ b/src/SMAPI/Framework/Models/ParsedModDataRecord.cs @@ -0,0 +1,48 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// A parsed representation of the fields from a for a specific manifest. + internal class ParsedModDataRecord + { + /********* + ** Accessors + *********/ + /// The underlying data record. + public ModDataRecord DataRecord { get; set; } + + /// The update key to apply. + public string UpdateKey { get; set; } + + /// The mod version to apply. + public ISemanticVersion Version { get; set; } + + /// The alternative URL the player can check for an updated version. + public string AlternativeUrl { get; set; } + + /// The predefined compatibility status. + public ModStatus Status { get; set; } = ModStatus.None; + + /// A reason phrase for the , or null to use the default reason. + public string StatusReasonPhrase { get; set; } + + /// The upper version for which the applies (if any). + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public string GetLocalVersionForUpdateChecks(string version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + } + } +} -- cgit From 2f101e716adae530d0451b1673a80fd25eced1b6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 22:11:20 -0500 Subject: encapsulate mod DB, add display name, and use in dependency checks (#439) --- docs/release-notes.md | 1 + src/SMAPI.Tests/Core/ModResolverTests.cs | 31 +- src/SMAPI/Framework/IModMetadata.cs | 2 +- src/SMAPI/Framework/ModData/ModDataField.cs | 82 ++ src/SMAPI/Framework/ModData/ModDataFieldKey.cs | 18 + src/SMAPI/Framework/ModData/ModDataRecord.cs | 141 +++ src/SMAPI/Framework/ModData/ModDatabase.cs | 168 +++ src/SMAPI/Framework/ModData/ModStatus.cs | 18 + src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 51 + src/SMAPI/Framework/ModLoading/ModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 48 +- src/SMAPI/Framework/Models/ModDataField.cs | 82 -- src/SMAPI/Framework/Models/ModDataFieldKey.cs | 18 - src/SMAPI/Framework/Models/ModDataRecord.cs | 243 ----- src/SMAPI/Framework/Models/ModStatus.cs | 18 - src/SMAPI/Framework/Models/ParsedModDataRecord.cs | 48 - src/SMAPI/Framework/Models/SConfig.cs | 5 +- src/SMAPI/Program.cs | 8 +- src/SMAPI/StardewModdingAPI.config.json | 1130 ++++++++++---------- src/SMAPI/StardewModdingAPI.csproj | 11 +- 20 files changed, 1105 insertions(+), 1020 deletions(-) create mode 100644 src/SMAPI/Framework/ModData/ModDataField.cs create mode 100644 src/SMAPI/Framework/ModData/ModDataFieldKey.cs create mode 100644 src/SMAPI/Framework/ModData/ModDataRecord.cs create mode 100644 src/SMAPI/Framework/ModData/ModDatabase.cs create mode 100644 src/SMAPI/Framework/ModData/ModStatus.cs create mode 100644 src/SMAPI/Framework/ModData/ParsedModDataRecord.cs delete mode 100644 src/SMAPI/Framework/Models/ModDataField.cs delete mode 100644 src/SMAPI/Framework/Models/ModDataFieldKey.cs delete mode 100644 src/SMAPI/Framework/Models/ModDataRecord.cs delete mode 100644 src/SMAPI/Framework/Models/ModStatus.cs delete mode 100644 src/SMAPI/Framework/Models/ParsedModDataRecord.cs (limited to 'src/SMAPI/Framework/Models') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6c4bdf94..c4c269eb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ # Release notes ## 2.5 * For players: + * Dependency errors will now show the name of the missing mod, instead of its ID. * Fixed mod crashes being logged under `[SMAPI]` instead of the mod name. * Updated compatibility list and enabled update checks for more older mods. diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 7c1efe53..900a6c4f 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -6,6 +6,7 @@ using Moq; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Serialisation; @@ -30,7 +31,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(rootFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); @@ -45,7 +46,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(modFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -84,7 +85,7 @@ namespace StardewModdingAPI.Tests.Core File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -233,7 +234,7 @@ namespace StardewModdingAPI.Tests.Core public void ProcessDependencies_NoMods_DoesNothing() { // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0], new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods."); @@ -249,7 +250,7 @@ namespace StardewModdingAPI.Tests.Core Mock modC = this.GetMetadata("Mod C"); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object, modC.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object, modC.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); @@ -266,7 +267,7 @@ namespace StardewModdingAPI.Tests.Core mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - new ModResolver().ProcessDependencies(new[] { mock.Object }); + new ModResolver().ProcessDependencies(new[] { mock.Object }, new ModDatabase()); // assert mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); @@ -285,7 +286,7 @@ namespace StardewModdingAPI.Tests.Core Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod A", "Mod B" }); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); @@ -305,7 +306,7 @@ namespace StardewModdingAPI.Tests.Core Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); @@ -331,7 +332,7 @@ namespace StardewModdingAPI.Tests.Core Mock modF = this.GetMetadata("Mod F", dependencies: new[] { "Mod C", "Mod E" }); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(6, mods.Length, 0, "Expected to get the same number of mods input."); @@ -358,7 +359,7 @@ namespace StardewModdingAPI.Tests.Core Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); @@ -382,7 +383,7 @@ namespace StardewModdingAPI.Tests.Core modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); @@ -401,7 +402,7 @@ namespace StardewModdingAPI.Tests.Core Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); @@ -417,7 +418,7 @@ namespace StardewModdingAPI.Tests.Core Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false); // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); @@ -434,7 +435,7 @@ namespace StardewModdingAPI.Tests.Core Mock 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(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); @@ -450,7 +451,7 @@ namespace StardewModdingAPI.Tests.Core Mock 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(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }, new ModDatabase()).ToArray(); // assert Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 41484567..a91b0a5b 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; namespace StardewModdingAPI.Framework diff --git a/src/SMAPI/Framework/ModData/ModDataField.cs b/src/SMAPI/Framework/ModData/ModDataField.cs new file mode 100644 index 00000000..fa8dd6d0 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Framework.ModData +{ + /// A versioned mod metadata field. + internal class ModDataField + { + /********* + ** Accessors + *********/ + /// The field key. + public ModDataFieldKey Key { get; } + + /// The field value. + public string Value { get; } + + /// Whether this field should only be applied if it's not already set. + public bool IsDefault { get; } + + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field key. + /// The field value. + /// Whether this field should only be applied if it's not already set. + /// The lowest version in the range, or null for all past versions. + /// The highest version in the range, or null for all future versions. + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// Get whether this data field applies for the given manifest. + /// The mod manifest. + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// Get whether a manifest field has a meaningful value for the purposes of enforcing . + /// The mod manifest. + /// The field key matching . + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs b/src/SMAPI/Framework/ModData/ModDataFieldKey.cs new file mode 100644 index 00000000..f68f575c --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.ModData +{ + /// The valid field keys. + public enum ModDataFieldKey + { + /// A manifest update key. + UpdateKey, + + /// An alternative URL the player can check for an updated version. + AlternativeUrl, + + /// The mod's predefined compatibility status. + Status, + + /// A reason phrase for the , or null to use the default reason. + StatusReasonPhrase + } +} diff --git a/src/SMAPI/Framework/ModData/ModDataRecord.cs b/src/SMAPI/Framework/ModData/ModDataRecord.cs new file mode 100644 index 00000000..79a954f7 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDataRecord.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Framework.ModData +{ + /// Raw mod metadata from SMAPI's internal mod list. + internal class ModDataRecord + { + /********* + ** Properties + *********/ + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + private IDictionary ExtensionData; + + + /********* + ** Accessors + *********/ + /// The mod's current unique ID. + public string ID { get; set; } + + /// The former mod IDs (if any). + /// + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// | character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// + public string FormerIDs { get; set; } + + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + /// The versioned field data. + /// + /// This maps field names to values. This should be accessed via . + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and Default, separated by pipes (whitespace trimmed). For example, Name + /// will always override the name, Default | Name will only override a blank + /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. + /// - The version format is min~max (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a value. + /// + public IDictionary Fields { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get a parsed representation of the . + public IEnumerable GetFields() + { + foreach (KeyValuePair pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public string GetLocalVersionForUpdateChecks(string version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + + /********* + ** Private methods + *********/ + /// The method invoked after JSON deserialisation. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + } +} diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs new file mode 100644 index 00000000..af067f8f --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDatabase.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.ModData +{ + /// Handles access to SMAPI's internal mod metadata list. + internal class ModDatabase + { + /********* + ** Properties + *********/ + /// The underlying mod data records indexed by default display name. + private readonly IDictionary Records; + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModDatabase() + : this(new Dictionary()) { } + + /// Construct an instance. + /// The underlying mod data records indexed by default display name. + public ModDatabase(IDictionary records) + { + this.Records = records; + } + + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ParsedModDataRecord GetParsed(IManifest manifest) + { + // get raw record + if (!this.TryGetRaw(manifest, out string displayName, out ModDataRecord record)) + return null; + + // parse fields + ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; + foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; + } + + /// Get the display name for a given mod ID (if available). + /// The unique mod ID. + public string GetDisplayNameFor(string id) + { + foreach (var entry in this.Records) + { + if (entry.Value.ID != null && entry.Value.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return entry.Key; + } + + return null; + } + + + /********* + ** Private models + *********/ + /// Get the data record matching a given manifest. + /// The mod manifest. + /// The mod's default display name. + /// The raw mod record. + private bool TryGetRaw(IManifest manifest, out string displayName, out ModDataRecord record) + { + if (manifest != null) + { + foreach (var entry in this.Records) + { + displayName = entry.Key; + record = entry.Value; + + // try main ID + if (record.ID != null && record.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (record.FormerIDs != null) + { + foreach (string part in record.FormerIDs.Split('|')) + { + // packed field snapshot + if (part.StartsWith("{")) + { + FieldSnapshot snapshot = JsonConvert.DeserializeObject(part); + bool isMatch = + (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase)) + && ( + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + + if (isMatch) + return true; + } + + // plain ID + else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + } + } + + displayName = null; + record = null; + return false; + } + + + /********* + ** Private models + *********/ + /// A unique set of fields which identifies the mod. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// The unique mod ID (or null to ignore it). + public string ID { get; set; } + + /// The entry DLL (or null to ignore it). + public string EntryDll { get; set; } + + /// The mod name (or null to ignore it). + public string Name { get; set; } + + /// The author name (or null to ignore it). + public string Author { get; set; } + } + } +} diff --git a/src/SMAPI/Framework/ModData/ModStatus.cs b/src/SMAPI/Framework/ModData/ModStatus.cs new file mode 100644 index 00000000..0e1d94d4 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.ModData +{ + /// Indicates how SMAPI should treat a mod. + internal enum ModStatus + { + /// Don't override the status. + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, + + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible + } +} diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs new file mode 100644 index 00000000..5a6561a7 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Framework.ModData +{ + /// A parsed representation of the fields from a for a specific manifest. + internal class ParsedModDataRecord + { + /********* + ** Accessors + *********/ + /// The underlying data record. + public ModDataRecord DataRecord { get; set; } + + /// The default mod name to display when the name isn't available (e.g. during dependency checks). + public string DisplayName { get; set; } + + /// The update key to apply. + public string UpdateKey { get; set; } + + /// The mod version to apply. + public ISemanticVersion Version { get; set; } + + /// The alternative URL the player can check for an updated version. + public string AlternativeUrl { get; set; } + + /// The predefined compatibility status. + public ModStatus Status { get; set; } = ModStatus.None; + + /// A reason phrase for the , or null to use the default reason. + public string StatusReasonPhrase { get; set; } + + /// The upper version for which the applies (if any). + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public string GetLocalVersionForUpdateChecks(string version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 1a71920e..29bb6617 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 6671e880..09a9299e 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; @@ -17,12 +18,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// Get manifest metadata for each folder in the given root path. /// The root path to search for mods. /// The JSON helper with which to read manifests. - /// Metadata about mods from SMAPI's internal data. + /// Handles access to SMAPI's internal mod metadata list. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable dataRecords) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) { - dataRecords = dataRecords.ToArray(); - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file @@ -54,22 +53,19 @@ namespace StardewModdingAPI.Framework.ModLoading } // parse internal data record (if any) - ParsedModDataRecord dataRecord = null; - if (manifest != null) - { - ModDataRecord rawDataRecord = dataRecords.FirstOrDefault(p => p.Matches(manifest)); - if (rawDataRecord != null) - dataRecord = rawDataRecord.ParseFieldsFor(manifest); - } + ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + + // get display name + string displayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = dataRecord?.DisplayName; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); // build metadata - string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) - ? manifest.Name - : modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); ModMetadataStatus status = error == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); } } @@ -193,7 +189,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. - public IEnumerable ProcessDependencies(IEnumerable mods) + /// Handles access to SMAPI's internal mod metadata list. + public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase) { // initialise metadata mods = mods.ToArray(); @@ -209,7 +206,7 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); + this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List()); return sortedMods.Reverse(); } @@ -220,12 +217,13 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. /// The full list of mods being validated. + /// Handles access to SMAPI's internal mod metadata list. /// The mod whose dependencies to process. /// The dependency state for each mod. /// The list in which to save mods sorted by dependency order. /// The current change of mod dependencies. /// Returns the mod dependency status. - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) { // check if already visited switch (states[mod]) @@ -276,11 +274,17 @@ namespace StardewModdingAPI.Framework.ModLoading // missing required dependencies, mark failed { - string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); - if (failedIDs.Any()) + string[] failedModNames = ( + from entry in dependencies + where entry.IsRequired && entry.Mod == null + let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + orderby displayName + select displayName + ).ToArray(); + if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -325,7 +329,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // recursively process each dependency - var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); + var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); switch (substatus) { // sorted successfully diff --git a/src/SMAPI/Framework/Models/ModDataField.cs b/src/SMAPI/Framework/Models/ModDataField.cs deleted file mode 100644 index 0812b39b..00000000 --- a/src/SMAPI/Framework/Models/ModDataField.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Framework.Models -{ - /// A versioned mod metadata field. - internal class ModDataField - { - /********* - ** Accessors - *********/ - /// The field key. - public ModDataFieldKey Key { get; } - - /// The field value. - public string Value { get; } - - /// Whether this field should only be applied if it's not already set. - public bool IsDefault { get; } - - /// The lowest version in the range, or null for all past versions. - public ISemanticVersion LowerVersion { get; } - - /// The highest version in the range, or null for all future versions. - public ISemanticVersion UpperVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The field key. - /// The field value. - /// Whether this field should only be applied if it's not already set. - /// The lowest version in the range, or null for all past versions. - /// The highest version in the range, or null for all future versions. - public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) - { - this.Key = key; - this.Value = value; - this.IsDefault = isDefault; - this.LowerVersion = lowerVersion; - this.UpperVersion = upperVersion; - } - - /// Get whether this data field applies for the given manifest. - /// The mod manifest. - public bool IsMatch(IManifest manifest) - { - return - manifest?.Version != null // ignore invalid manifest - && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) - && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); - } - - - /********* - ** Private methods - *********/ - /// Get whether a manifest field has a meaningful value for the purposes of enforcing . - /// The mod manifest. - /// The field key matching . - private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) - { - switch (key) - { - // update key - case ModDataFieldKey.UpdateKey: - return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(); - - // non-manifest fields - case ModDataFieldKey.AlternativeUrl: - case ModDataFieldKey.StatusReasonPhrase: - case ModDataFieldKey.Status: - return false; - - default: - return false; - } - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataFieldKey.cs b/src/SMAPI/Framework/Models/ModDataFieldKey.cs deleted file mode 100644 index 5767afc9..00000000 --- a/src/SMAPI/Framework/Models/ModDataFieldKey.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// The valid field keys. - public enum ModDataFieldKey - { - /// A manifest update key. - UpdateKey, - - /// An alternative URL the player can check for an updated version. - AlternativeUrl, - - /// The mod's predefined compatibility status. - Status, - - /// A reason phrase for the , or null to use the default reason. - StatusReasonPhrase - } -} diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs deleted file mode 100644 index 2c26741c..00000000 --- a/src/SMAPI/Framework/Models/ModDataRecord.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Framework.Models -{ - /// Raw mod metadata from SMAPI's internal mod list. - internal class ModDataRecord - { - /********* - ** Properties - *********/ - /// This field stores properties that aren't mapped to another field before they're parsed into . - [JsonExtensionData] - private IDictionary ExtensionData; - - - /********* - ** Accessors - *********/ - /// The mod's current unique ID. - public string ID { get; set; } - - /// The former mod IDs (if any). - /// - /// This uses a custom format which uniquely identifies a mod across multiple versions and - /// supports matching other fields if no ID was specified. This doesn't include the latest - /// ID, if any. Format rules: - /// 1. If the mod's ID changed over time, multiple variants can be separated by the - /// | character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and - /// EntryDll) to match. - /// - public string FormerIDs { get; set; } - - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - - /// The versioned field data. - /// - /// This maps field names to values. This should be accessed via . - /// Format notes: - /// - Each key consists of a field name prefixed with any combination of version range - /// and Default, separated by pipes (whitespace trimmed). For example, Name - /// will always override the name, Default | Name will only override a blank - /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. - /// - The version format is min~max (where either side can be blank for unbounded), or - /// a single version number. - /// - The field name itself corresponds to a value. - /// - public IDictionary Fields { get; set; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Get whether the manifest matches the field. - /// The mod manifest to check. - public bool Matches(IManifest manifest) - { - // try main ID - if (this.ID != null && this.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - if (this.FormerIDs != null) - { - foreach (string part in this.FormerIDs.Split('|')) - { - // packed field snapshot - if (part.StartsWith("{")) - { - FieldSnapshot snapshot = JsonConvert.DeserializeObject(part); - bool isMatch = - (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase)) - && ( - snapshot.Author == null - || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); - - if (isMatch) - return true; - } - - // plain ID - else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - } - - // no match - return false; - } - - /// Get a parsed representation of the . - public IEnumerable GetFields() - { - foreach (KeyValuePair pair in this.Fields) - { - // init fields - string packedKey = pair.Key; - string value = pair.Value; - bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; - - // parse - string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); - ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); - foreach (string part in parts.Take(parts.Length - 1)) - { - // 'default' - if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) - { - isDefault = true; - continue; - } - - // version range - if (part.Contains("~")) - { - string[] versionParts = part.Split(new[] { '~' }, 2); - lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; - upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; - continue; - } - - // single version - lowerVersion = new SemanticVersion(part); - upperVersion = new SemanticVersion(part); - } - - yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); - } - } - - /// Get a parsed representation of the which match a given manifest. - /// The manifest to match. - public ParsedModDataRecord ParseFieldsFor(IManifest manifest) - { - ParsedModDataRecord parsed = new ParsedModDataRecord { DataRecord = this }; - foreach (ModDataField field in this.GetFields().Where(field => field.IsMatch(manifest))) - { - switch (field.Key) - { - // update key - case ModDataFieldKey.UpdateKey: - parsed.UpdateKey = field.Value; - break; - - // alternative URL - case ModDataFieldKey.AlternativeUrl: - parsed.AlternativeUrl = field.Value; - break; - - // status - case ModDataFieldKey.Status: - parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); - parsed.StatusUpperVersion = field.UpperVersion; - break; - - // status reason phrase - case ModDataFieldKey.StatusReasonPhrase: - parsed.StatusReasonPhrase = field.Value; - break; - } - } - - return parsed; - } - - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public string GetLocalVersionForUpdateChecks(string version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) - { - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - - - /********* - ** Private methods - *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) - { - if (this.ExtensionData != null) - { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; - } - } - - - /********* - ** Private models - *********/ - /// A unique set of fields which identifies the mod. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] - private class FieldSnapshot - { - /********* - ** Accessors - *********/ - /// The unique mod ID (or null to ignore it). - public string ID { get; set; } - - /// The entry DLL (or null to ignore it). - public string EntryDll { get; set; } - - /// The mod name (or null to ignore it). - public string Name { get; set; } - - /// The author name (or null to ignore it). - public string Author { get; set; } - } - } -} diff --git a/src/SMAPI/Framework/Models/ModStatus.cs b/src/SMAPI/Framework/Models/ModStatus.cs deleted file mode 100644 index 343ccb7e..00000000 --- a/src/SMAPI/Framework/Models/ModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Indicates how SMAPI should treat a mod. - internal enum ModStatus - { - /// Don't override the status. - None, - - /// The mod is obsolete and shouldn't be used, regardless of version. - Obsolete, - - /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken, - - /// Assume the mod is compatible, even if SMAPI detects incompatible code. - AssumeCompatible - } -} diff --git a/src/SMAPI/Framework/Models/ParsedModDataRecord.cs b/src/SMAPI/Framework/Models/ParsedModDataRecord.cs deleted file mode 100644 index 0abc7b89..00000000 --- a/src/SMAPI/Framework/Models/ParsedModDataRecord.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A parsed representation of the fields from a for a specific manifest. - internal class ParsedModDataRecord - { - /********* - ** Accessors - *********/ - /// The underlying data record. - public ModDataRecord DataRecord { get; set; } - - /// The update key to apply. - public string UpdateKey { get; set; } - - /// The mod version to apply. - public ISemanticVersion Version { get; set; } - - /// The alternative URL the player can check for an updated version. - public string AlternativeUrl { get; set; } - - /// The predefined compatibility status. - public ModStatus Status { get; set; } = ModStatus.None; - - /// A reason phrase for the , or null to use the default reason. - public string StatusReasonPhrase { get; set; } - - /// The upper version for which the applies (if any). - public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public string GetLocalVersionForUpdateChecks(string version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) - { - return this.DataRecord.GetRemoteVersionForUpdateChecks(version); - } - } -} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 401e1a3a..17169714 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.ModData; + namespace StardewModdingAPI.Framework.Models { /// The SMAPI configuration settings. @@ -22,6 +25,6 @@ namespace StardewModdingAPI.Framework.Models public bool VerboseLogging { get; set; } /// Extra metadata about mods. - public ModDataRecord[] ModData { get; set; } + public IDictionary ModData { get; set; } } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b5ce3033..ec841f4c 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -19,6 +19,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; @@ -349,17 +350,20 @@ namespace StardewModdingAPI if (!this.ValidateContentIntegrity()) this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); + // load mod data + ModDatabase modDatabase = new ModDatabase(this.Settings.ModData); + // load mods { this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), modDatabase).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); // process dependencies - mods = resolver.ProcessDependencies(mods).ToArray(); + mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods this.LoadMods(mods, new JsonHelper(), this.ContentManager); diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 7518d6b3..8b92f277 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -44,7 +44,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * * Standard fields * =============== - * The predefined fields are documented below (only 'ID' is required). + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). * * - ID: the mod's latest unique ID (if any). * @@ -91,1718 +92,1719 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the * mod is no longer compatible. */ - "ModData": [ - { - // AccessChestAnywhere + "ModData": { + "AccessChestAnywhere": { "ID": "AccessChestAnywhere", "MapLocalVersions": { "1.1-1078": "1.1" }, "Default | UpdateKey": "Nexus:257", - "~1.1 | Status": "AssumeBroken", + "~1.1 | Status": "AssumeBroken", "~1.1 | AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // AdjustArtisanPrices + + "AdjustArtisanPrices": { "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", "Default | UpdateKey": "Chucklefish:3532", - "~0.1 | Status": "AssumeBroken", + "~0.1 | Status": "AssumeBroken", "~0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Adjust Monster + + "Adjust Monster": { "ID": "mmanlapat.AdjustMonster", "Default | UpdateKey": "Nexus:1161" }, - { - // Advanced Location Loader + + "Advanced Location Loader": { "ID": "Entoarox.AdvancedLocationLoader", "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Adventure Shop Inventory + + "Adventure Shop Inventory": { "ID": "HammurabiAdventureShopInventory", "Default | UpdateKey": "Chucklefish:4608" }, - { - // AgingMod + + "AgingMod": { "ID": "skn.AgingMod", "Default | UpdateKey": "Nexus:1129", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // All Crops All Seasons + + "All Crops All Seasons": { "ID": "cantorsdust.AllCropsAllSeasons", "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 "Default | UpdateKey": "Nexus:170" }, - { - // All Professions + + "All Professions": { "ID": "cantorsdust.AllProfessions", "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 "Default | UpdateKey": "Nexus:174" }, - { - // Almighty Tool + + "Almighty Tool": { "ID": "439", "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 "MapRemoteVersions": { "1.21": "1.2.1" }, "Default | UpdateKey": "Nexus:439", "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Animal Mood Fix + + "Animal Husbandry": { + "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 + "Default | UpdateKey": "Nexus:1538" + }, + + "Animal Mood Fix": { "ID": "GPeters-AnimalMoodFix", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." }, - { - // Animal Sitter + + "Animal Sitter": { "ID": "jwdred.AnimalSitter", "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 "Default | UpdateKey": "Nexus:581", "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // A Tapper's Dream + + "A Tapper's Dream": { "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", "Default | UpdateKey": "Nexus:260", "~1.4 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.4 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Auto Animal Doors + + "Auto Animal Doors": { "ID": "AaronTaggart.AutoAnimalDoors", "MapRemoteVersions": { "1.1.1": "1.1" }, // manifest not updated "Default | UpdateKey": "Nexus:1019" }, - { - // Auto-Eat + + "Auto-Eat": { "ID": "Permamiss.AutoEat", "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 "Default | UpdateKey": "Nexus:643" }, - { - // AutoGate + + "AutoGate": { "ID": "AutoGate", "Default | UpdateKey": "Nexus:820" }, - { - // Automate + + "Automate": { "ID": "Pathoschild.Automate", "Default | UpdateKey": "Nexus:1063" }, - { - // Automated Doors + + "Automated Doors": { "ID": "azah.automated-doors", "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 "MapLocalVersions": { "1.4.1-1": "1.4.1" }, "Default | UpdateKey": "GitHub:azah/AutomatedDoors" }, - { - // AutoSpeed + + "AutoSpeed": { "ID": "Omegasis.AutoSpeed", "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:443" // added in 1.4.1 }, - { - // Basic Sprinkler Improved + + "Basic Sprinklers Improved": { "ID": "lrsk_sdvm_bsi.0117171308", "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated "Default | UpdateKey": "Nexus:833" }, - { - // Better Hay + + "Better Hay": { "ID": "cat.betterhay", "Default | UpdateKey": "Nexus:1430" }, - { - // Better Quality More Seasons + + "Better Quality More Seasons": { "ID": "SB_BQMS", "Default | UpdateKey": "Nexus:935" }, - { - // Better Quarry + + "Better Quarry": { "ID": "BetterQuarry", "Default | UpdateKey": "Nexus:771" }, - { - // Better Ranching + + "Better Ranching": { "ID": "BetterRanching", "Default | UpdateKey": "Nexus:859" }, - { - // Better Shipping Box + + "Better Shipping Box": { "ID": "Kithio:BetterShippingBox", "MapLocalVersions": { "1.0.1": "1.0.2" }, "Default | UpdateKey": "Chucklefish:4302", "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Better Sprinklers + + "Better Sprinklers": { "ID": "Speeder.BetterSprinklers", "FormerIDs": "SPDSprinklersMod", // changed in 2.3 "Default | UpdateKey": "Nexus:41", "~2.3.1-pathoschild-update | Status": "AssumeBroken", // broke in SDV 1.2 "~2.3.1-pathoschild-update | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Billboard Anywhere + + "Billboard Anywhere": { "ID": "Omegasis.BillboardAnywhere", "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:492" // added in 1.4.1 }, - { - // Birthday Mail + + "Birthday Mail": { "ID": "KathrynHazuka.BirthdayMail", "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update "Default | UpdateKey": "Nexus:276", "~1.2.2 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Breed Like Rabbits + + "Breed Like Rabbits": { "ID": "dycedarger.breedlikerabbits", "Default | UpdateKey": "Nexus:948" }, - { - // Build Endurance + + "Build Endurance": { "ID": "Omegasis.BuildEndurance", "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:445", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Build Health + + "Build Health": { "ID": "Omegasis.BuildHealth", "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:446", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Butcher Mod - "ID": "DIGUS.BUTCHER", - "Default | UpdateKey": "Nexus:1538" - }, - { - // Buy Cooking Recipes + + "Buy Cooking Recipes": { "ID": "Denifia.BuyRecipes", "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Buy Back Collectables + + "Buy Back Collectables": { "ID": "Omegasis.BuyBackCollectables", "FormerIDs": "BuyBackCollectables", // changed in 1.4 "Default | UpdateKey": "Nexus:507", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Carry Chest + + "Carry Chest": { "ID": "spacechase0.CarryChest", "Default | UpdateKey": "Nexus:1333" }, - { - // Casks Anywhere + + "Casks Anywhere": { "ID": "CasksAnywhere", "MapLocalVersions": { "1.1-alpha": "1.1" }, "Default | UpdateKey": "Nexus:878" }, - { - // Categorize Chests + + "Categorize Chests": { "ID": "CategorizeChests", "Default | UpdateKey": "Nexus:1300" }, - { - // ChefsCloset + + "Chefs Closet": { "ID": "Duder.ChefsCloset", "MapLocalVersions": { "1.3-1": "1.3" }, "Default | UpdateKey": "Nexus:1030" }, - { - // Chest Label System + + "Chest Label System": { "ID": "Speeder.ChestLabel", "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update "Default | UpdateKey": "Nexus:242", "~1.6 | Status": "AssumeBroken", // broke in SDV 1.1 "~1.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Chest Pooling + + "Chest Pooling": { "ID": "mralbobo.ChestPooling", "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Chests Anywhere + + "Chests Anywhere": { "ID": "Pathoschild.ChestsAnywhere", "FormerIDs": "ChestsAnywhere", // changed in 1.9 "Default | UpdateKey": "Nexus:518", "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Choose Baby Gender + + "Choose Baby Gender": { "ID": "{EntryDll: 'ChooseBabyGender.dll'}", "Default | UpdateKey": "Nexus:590", "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // CJB Automation + + "CJB Automation": { "ID": "CJBAutomation", "Default | UpdateKey": "Nexus:211", "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" }, - { - // CJB Cheats Menu + + "CJB Cheats Menu": { "ID": "CJBok.CheatsMenu", "FormerIDs": "CJBCheatsMenu", // changed in 1.14 "Default | UpdateKey": "Nexus:4", "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 }, - { - // CJB Item Spawner + + "CJB Item Spawner": { "ID": "CJBok.ItemSpawner", "FormerIDs": "CJBItemSpawner", // changed in 1.7 "Default | UpdateKey": "Nexus:93", "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 }, - { - // CJB Show Item Sell Price + + "CJB Show Item Sell Price": { "ID": "CJBok.ShowItemSellPrice", "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 "Default | UpdateKey": "Nexus:5", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Clean Farm + + "Clean Farm": { "ID": "tstaples.CleanFarm", "Default | UpdateKey": "Nexus:794" }, - { - // Climates of Ferngill + + "Climates of Ferngill": { "ID": "KoihimeNakamura.ClimatesOfFerngill", "Default | UpdateKey": "Nexus:604", "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Cold Weather Haley + + "Cold Weather Haley": { "ID": "LordXamon.ColdWeatherHaleyPRO", "Default | UpdateKey": "Nexus:1169", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Colored Chests + + "Colored Chests": { "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." }, - { - // Combat with Farm Implements + + "Combat with Farm Implements": { "ID": "SPDFarmingImplementsInCombat", "Default | UpdateKey": "Nexus:313", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Community Bundle Item Tooltip + + "Community Bundle Item Tooltip": { "ID": "musbah.bundleTooltip", "Default | UpdateKey": "Nexus:1329" }, - { - // Concentration on Farming + + "Concentration on Farming": { "ID": "punyo.ConcentrationOnFarming", "Default | UpdateKey": "Nexus:1445" }, - { - // Configurable Machines + + "Configurable Machines": { "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", "MapLocalVersions": { "1.2-beta": "1.2" }, "Default | UpdateKey": "Nexus:280" }, - { - // Configurable Shipping Dates + + "Configurable Shipping Dates": { "ID": "ConfigurableShippingDates", "Default | UpdateKey": "Nexus:675", "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Cooking Skill + + "Cooking Skill": { "ID": "spacechase0.CookingSkill", "FormerIDs": "CookingSkill", // changed in 1.0.4–6 "Default | UpdateKey": "Nexus:522", "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // CrabNet + + "CrabNet": { "ID": "jwdred.CrabNet", "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 "Default | UpdateKey": "Nexus:584", "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Current Location + + "Current Location": { "ID": "CurrentLocation102120161203", "Default | UpdateKey": "Nexus:638" }, - { - // Custom Critters + + "Custom Critters": { "ID": "spacechase0.CustomCritters", "Default | UpdateKey": "Nexus:1255" }, - { - // Custom Element Handler + + "Custom Element Handler": { "ID": "Platonymous.CustomElementHandler", "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 }, - { - // Custom Farming + + "Custom Farming": { "ID": "Platonymous.CustomFarming", "Default | UpdateKey": "Nexus:991" // added in 0.6.1 }, - { - // Custom Farming Automate Bridge + + "Custom Farming Automate Bridge": { "ID": "Platonymous.CFAutomate", "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" }, - { - // Custom Farm Types + + "Custom Farm Types": { "ID": "spacechase0.CustomFarmTypes", "Default | UpdateKey": "Nexus:1140" }, - { - // Custom Furniture + + "Custom Furniture": { "ID": "Platonymous.CustomFurniture", "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 }, - { - // Customize Exterior + + "Customize Exterior": { "ID": "spacechase0.CustomizeExterior", "FormerIDs": "CustomizeExterior", // changed in 1.0.3 "Default | UpdateKey": "Nexus:1099", "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Customizable Cart Redux + + + "Customizable Cart Redux": { "ID": "KoihimeNakamura.CCR", "MapLocalVersions": { "1.1-20170917": "1.1" }, "Default | UpdateKey": "Nexus:1402" }, - { - // Customizable Traveling Cart Days + + "Customizable Traveling Cart Days": { "ID": "TravelingCartYyeahdude", "Default | UpdateKey": "Nexus:567", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Custom Linens + + "Custom Linens": { "ID": "Mevima.CustomLinens", "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated "Default | UpdateKey": "Nexus:1027" }, - { - // Custom Shops Redux + + "Custom Shops Redux": { "ID": "Omegasis.CustomShopReduxGui", "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 }, - { - // Custom TV + + "Custom TV": { "ID": "Platonymous.CustomTV", "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 }, - { - // Daily Luck Message + + "Daily Luck Message": { "ID": "Schematix.DailyLuckMessage", "Default | UpdateKey": "Nexus:1327" }, - { - // Daily News + + "Daily News": { "ID": "bashNinja.DailyNews", "Default | UpdateKey": "Nexus:1141", "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Daily Quest Anywhere + + "Daily Quest Anywhere": { "ID": "Omegasis.DailyQuestAnywhere", "FormerIDs": "DailyQuest", // changed in 1.4 "Default | UpdateKey": "Nexus:513" // added in 1.4.1 }, - { - // Debug Mode + + "Debug Mode": { "ID": "Pathoschild.DebugMode", "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 "Default | UpdateKey": "Nexus:679" }, - { - // Dynamic Checklist + + "Dynamic Checklist": { "ID": "gunnargolf.DynamicChecklist", "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Dynamic Horses + + "Dynamic Horses": { "ID": "Bpendragon-DynamicHorses", "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated "Default | UpdateKey": "Nexus:874" }, - { - // Dynamic Machines + + "Dynamic Machines": { "ID": "DynamicMachines", "MapLocalVersions": { "1.1": "1.1.1" }, "Default | UpdateKey": "Nexus:374", "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Dynamic NPC Sprites + + "Dynamic NPC Sprites": { "ID": "BashNinja.DynamicNPCSprites", "Default | UpdateKey": "Nexus:1183" }, - { - // Easier Farming + + "Easier Farming": { "ID": "cautiouswafffle.EasierFarming", "Default | UpdateKey": "Nexus:1426" }, - { - // Empty Hands + + "Empty Hands": { "ID": "QuicksilverFox.EmptyHands", "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Enemy Health Bars + + "Enemy Health Bars": { "ID": "Speeder.HealthBars", "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update "Default | UpdateKey": "Nexus:193", "~1.7 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.7 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Entoarox Framework + + "Entoarox Framework": { "ID": "Entoarox.EntoaroxFramework", "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) "~1.7.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Expanded Fridge / Dynamic Expanded Fridge + + "Expanded Fridge": { "ID": "Uwazouri.ExpandedFridge", "Default | UpdateKey": "Nexus:1191" }, - { - // Experience Bars + + "Experience Bars": { "ID": "spacechase0.ExperienceBars", "FormerIDs": "ExperienceBars", // changed in 1.0.2 "Default | UpdateKey": "Nexus:509" }, - { - // Extended Bus System + + "Extended Bus System": { "ID": "ExtendedBusSystem", "Default | UpdateKey": "Chucklefish:4373" }, - { - // Extended Fridge + + "Extended Fridge": { "ID": "Crystalmir.ExtendedFridge", "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 "Default | UpdateKey": "Nexus:485", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Extended Greenhouse + + "Extended Greenhouse": { "ID": "ExtendedGreenhouse", "Default | UpdateKey": "Chucklefish:4303", "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Extended Minecart + + "Extended Minecart": { "ID": "Entoarox.ExtendedMinecart", "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) }, - { - // Extended Reach + + "Extended Reach": { "ID": "spacechase0.ExtendedReach", "Default | UpdateKey": "Nexus:1493" }, - { - // Fall 28 Snow Day + + "Fall 28 Snow Day": { "ID": "Omegasis.Fall28SnowDay", "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:486", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Farm Automation: Barn Door Automation + + "Farm Automation: Barn Door Automation": { "ID": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Farm Automation: Item Collector + + "Farm Automation: Item Collector": { "ID": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Farm Automation Unofficial: Item Collector + + "Farm Automation Unofficial: Item Collector": { "ID": "Maddy99.FarmAutomation.ItemCollector", "~0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Farm Expansion + + "Farm Expansion": { "ID": "Advize.FarmExpansion", "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 "Default | UpdateKey": "Nexus:130", "~2.0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~2.0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Farm Resource Generator + + "Farm Resource Generator": { "ID": "{EntryDll: 'FarmResourceGenerator.dll'}", "Default | UpdateKey": "Nexus:647", "~1.0.4 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.4 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Fast Animations + + "Fast Animations": { "ID": "Pathoschild.FastAnimations", "Default | UpdateKey": "Nexus:1089" }, - { - // Faster Paths + + "Faster Paths": { "ID": "Entoarox.FasterPaths", "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) }, - { - // Faster Run + + "Faster Run": { "ID": "KathrynHazuka.FasterRun", "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Fishing Adjust + + "Fishing Adjust": { "ID": "shuaiz.FishingAdjustMod", "Default | UpdateKey": "Nexus:1350" }, - { - // Fishing Tuner Redux + + "Fishing Tuner Redux": { "ID": "HammurabiFishingTunerRedux", "Default | UpdateKey": "Chucklefish:4578" }, - { - // FlorenceMod + + "FlorenceMod": { "ID": "{EntryDll: 'FlorenceMod.dll'}", "MapLocalVersions": { "1.0.1": "1.1" }, "Default | UpdateKey": "Nexus:591", "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Flower Color Picker + + "Flower Color Picker": { "ID": "spacechase0.FlowerColorPicker", "Default | UpdateKey": "Nexus:1229" }, - { - // Forage at the Farm + + "Forage at the Farm": { "ID": "ForageAtTheFarm", "Default | UpdateKey": "Nexus:673", "~1.5.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.5.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Furniture Anywhere + + "Furniture Anywhere": { "ID": "Entoarox.FurnitureAnywhere", "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) }, - { - // Game Reminder + + "Game Reminder": { "ID": "mmanlapat.GameReminder", "Default | UpdateKey": "Nexus:1153" }, - { - // Gate Opener + + "Gate Opener": { "ID": "mralbobo.GateOpener", "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // GenericShopExtender + + "GenericShopExtender": { "ID": "GenericShopExtender", "Default | UpdateKey": "Nexus:814", // added in 0.1.3 "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Geode Info Menu + + "Geode Info Menu": { "ID": "cat.geodeinfomenu", "Default | UpdateKey": "Nexus:1448" }, - { - // Get Dressed + + "Get Dressed": { "ID": "Advize.GetDressed", "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 "Default | UpdateKey": "Nexus:331", "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Giant Crop Ring + + "Giant Crop Ring": { "ID": "cat.giantcropring", "Default | UpdateKey": "Nexus:1182" }, - { - // Gift Taste Helper + + "Gift Taste Helper": { "ID": "tstaples.GiftTasteHelper", "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 "Default | UpdateKey": "Nexus:229", "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Grandfather's Gift + + "Grandfather's Gift": { "ID": "ShadowDragon.GrandfathersGift", "Default | UpdateKey": "Nexus:985" }, - { - // Happy Animals + + "Happy Animals": { "ID": "HappyAnimals", "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Happy Birthday (Omegasis) + + "Happy Birthday (Omegasis)": { "ID": "Omegasis.HappyBirthday", "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork "Default | UpdateKey": "Nexus:520", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Happy Birthday (Oxyligen fork) + + "Happy Birthday (Oxyligen fork)": { "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork "Default | UpdateKey": "Nexus:1064" }, - { - // Harp of Yoba Redux + + "Harp of Yoba Redux": { "ID": "Platonymous.HarpOfYobaRedux", "Default | UpdateKey": "Nexus:914" // added in 2.0.3 }, - { - // Harvest Moon Witch Princess + + "Harvest Moon Witch Princess": { "ID": "Sasara.WitchPrincess", "Default | UpdateKey": "Nexus:1157" }, - { - // Harvest With Scythe + + "Harvest With Scythe": { "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", "Default | UpdateKey": "Nexus:236", "~1.0.6 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Horse Whistle (icepuente) + + "Horse Whistle (icepuente)": { "ID": "icepuente.HorseWhistle", "Default | UpdateKey": "Nexus:1131" }, - { - // Hunger (Yyeadude) + + "Hunger (Yyeadude)": { "ID": "HungerYyeadude", "Default | UpdateKey": "Nexus:613" }, - { - // Hunger for Food (Tigerle) + + "Hunger for Food (Tigerle)": { "ID": "HungerForFoodByTigerle", "Default | UpdateKey": "Nexus:810", "~0.1.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~0.1.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Hunger Mod (skn) + + "Hunger Mod (skn)": { "ID": "skn.HungerMod", "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated "Default | UpdateKey": "Nexus:1127" }, - { - // Idle Pause + + "Idle Pause": { "ID": "Veleek.IdlePause", "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated "Default | UpdateKey": "Nexus:1092" }, - { - // Improved Quality of Life + + "Improved Quality of Life": { "ID": "Demiacle.ImprovedQualityOfLife", "Default | UpdateKey": "Nexus:1025", "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Instant Geode + + "Instant Geode": { "ID": "InstantGeode", "~1.12 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Instant Grow Trees + + "Instant Grow Trees": { "ID": "cantorsdust.InstantGrowTrees", "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 "Default | UpdateKey": "Nexus:173" }, - { - // Interaction Helper + + "Interaction Helper": { "ID": "HammurabiInteractionHelper", "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Item Auto Stacker + + "Item Auto Stacker": { "ID": "cat.autostacker", "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated "Default | UpdateKey": "Nexus:1184" }, - { - // Jiggly Junimo Bundles + + "Jiggly Junimo Bundles": { "ID": "Greger.JigglyJunimoBundles", "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update }, - { - // Junimo Farm + + "Junimo Farm": { "ID": "Platonymous.JunimoFarm", "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated "Default | UpdateKey": "Nexus:984" // added in 1.1.3 }, - { - // Less Strict Over-Exertion (AntiExhaustion) + + "Less Strict Over-Exertion (AntiExhaustion)": { "ID": "BALANCEMOD_AntiExhaustion", "MapLocalVersions": { "0.0": "1.1" }, "Default | UpdateKey": "Nexus:637", "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Level Extender + + "Level Extender": { "ID": "Devin Lematty.Level Extender", "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated "Default | UpdateKey": "Nexus:1471" }, - { - // Level Up Notifications + + "Level Up Notifications": { "ID": "Level Up Notifications", "Default | UpdateKey": "Nexus:855" }, - { - // Location and Music Logging + + "Location and Music Logging": { "ID": "Brandy Lover.LMlog", "Default | UpdateKey": "Nexus:1366" }, - { - // Longevity + + "Longevity": { "ID": "RTGOAT.Longevity", "Default | UpdateKey": "Nexus:649" }, - { - // Lookup Anything + + "Lookup Anything": { "ID": "Pathoschild.LookupAnything", "FormerIDs": "LookupAnything", // changed in 1.10.1 "Default | UpdateKey": "Nexus:541", "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Love Bubbles + + "Love Bubbles": { "ID": "LoveBubbles", "Default | UpdateKey": "Nexus:1318" }, - { - // Loved Labels + + "Loved Labels": { "ID": "Advize.LovedLabels", "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 "Default | UpdateKey": "Nexus:279", "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Luck Skill + + "Luck Skill": { "ID": "spacechase0.LuckSkill", "FormerIDs": "LuckSkill", // changed in 0.1.4 "Default | UpdateKey": "Nexus:521", "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Mail Framework + + "Mail Framework": { "ID": "DIGUS.MailFrameworkMod", "Default | UpdateKey": "Nexus:1536" }, - { - // MailOrderPigs + + "MailOrderPigs": { "ID": "jwdred.MailOrderPigs", "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 "Default | UpdateKey": "Nexus:632", "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Makeshift Multiplayer + + "Makeshift Multiplayer": { "ID": "spacechase0.StardewValleyMP", "FormerIDs": "StardewValleyMP", // changed in 0.3 "Default | UpdateKey": "Nexus:501", "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Map Image Exporter + + "Map Image Exporter": { "ID": "spacechase0.MapImageExporter", "FormerIDs": "MapImageExporter", // changed in 1.0.2 "Default | UpdateKey": "Nexus:1073" }, - { - // Message Box [API]? (ChatMod) + + "Message Box [API]? (ChatMod)": { "ID": "Kithio:ChatMod", "Default | UpdateKey": "Chucklefish:4296", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Mining at the Farm + + "Mining at the Farm": { "ID": "MiningAtTheFarm", "Default | UpdateKey": "Nexus:674" }, - { - // Mining With Explosives + + "Mining With Explosives": { "ID": "MiningWithExplosives", "Default | UpdateKey": "Nexus:770" }, - { - // Modder Serialization Utility + + "Modder Serialization Utility": { "ID": "SerializerUtils-0-1", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it's no longer maintained or used." }, - { - // More Animals + + "More Animals": { "ID": "Entoarox.MoreAnimals", "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility }, - { - // More Artifact Spots + + "More Artifact Spots": { "ID": "451", "Default | UpdateKey": "Nexus:451", "~1.0.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // More Map Layers + + "More Map Layers": { "ID": "Platonymous.MoreMapLayers", "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 }, - { - // More Rain + + "More Rain": { "ID": "Omegasis.MoreRain", "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:441", // added in 1.5.1 "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // More Weapons + + "More Weapons": { "ID": "Joco80.MoreWeapons", "Default | UpdateKey": "Nexus:1168" }, - { - // Move Faster + + "Move Faster": { "ID": "shuaiz.MoveFasterMod", "Default | UpdateKey": "Nexus:1351" }, - { - // Multiple Sprites and Portraits On Rotation (File Loading) + + "Multiple Sprites and Portraits On Rotation (File Loading)": { "ID": "FileLoading", "MapLocalVersions": { "1.1": "1.12" }, "Default | UpdateKey": "Nexus:1094", "~1.12 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Museum Rearranger + + "Museum Rearranger": { "ID": "Omegasis.MuseumRearranger", "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:428", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // New Machines + + "New Machines": { "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", "Default | UpdateKey": "Chucklefish:3683", "~4.2.1343 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~4.2.1343 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Night Owl + + "Night Owl": { "ID": "Omegasis.NightOwl", "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest "Default | UpdateKey": "Nexus:433", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // No Kids Ever + + "No Kids Ever": { "ID": "Hangy.NoKidsEver", "Default | UpdateKey": "Nexus:1464" }, - { - // No Debug Mode + + "No Debug Mode": { "ID": "NoDebugMode", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." }, - { - // No Fence Decay + + "No Fence Decay": { "ID": "cat.nofencedecay", "Default | UpdateKey": "Nexus:1180" }, - { - // No More Pets + + "No More Pets": { "ID": "Omegasis.NoMorePets", "FormerIDs": "NoMorePets", // changed in 1.4 "Default | UpdateKey": "Nexus:506" // added in 1.4.1 }, - { - // NoSoilDecay + + "NoSoilDecay": { "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", "Default | UpdateKey": "Nexus:237", "~0.5 | Status": "AssumeBroken", // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // No Soil Decay Redux + + "No Soil Decay Redux": { "ID": "Platonymous.NoSoilDecayRedux", "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 }, - { - // NPC Map Locations + + "NPC Map Locations": { "ID": "NPCMapLocationsMod", "Default | UpdateKey": "Nexus:239", "1.42~1.43 | Status": "AssumeBroken", "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." }, - { - // NPC Speak + + "NPC Speak": { "ID": "{EntryDll: 'NpcEcho.dll'}", "Default | UpdateKey": "Nexus:694", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Object Time Left + + "Object Time Left": { "ID": "spacechase0.ObjectTimeLeft", "Default | UpdateKey": "Nexus:1315" }, - { - // OmniFarm + + "OmniFarm": { "ID": "PhthaloBlue.OmniFarm", "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Out of Season Bonuses / Seasonal Items + + "Out of Season Bonuses (Seasonal Items)": { "ID": "midoriarmstrong.seasonalitems", "Default | UpdateKey": "Nexus:1452" }, - { - // Part of the Community + + "Part of the Community": { "ID": "SB_PotC", "Default | UpdateKey": "Nexus:923", "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // PelicanFiber + + "PelicanFiber": { "ID": "jwdred.PelicanFiber", "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version "Default | UpdateKey": "Nexus:631", "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // PelicanTTS + + "PelicanTTS": { "ID": "Platonymous.PelicanTTS", "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 "~1.6 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Persia the Mermaid - Standalone Custom NPC + + "Persia the Mermaid - Standalone Custom NPC": { "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", "Default | UpdateKey": "Nexus:1419" }, - { - // Persival's BundleMod + + "Persival's BundleMod": { "ID": "{EntryDll: 'BundleMod.dll'}", "Default | UpdateKey": "Nexus:438", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Plant on Grass + + "Plant on Grass": { "ID": "Demiacle.PlantOnGrass", "Default | UpdateKey": "Nexus:1026" }, - { - // Point-and-Plant + + "Point-and-Plant": { "ID": "jwdred.PointAndPlant", "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 "Default | UpdateKey": "Nexus:572", "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Pony Weight Loss Program + + "Pony Weight Loss Program": { "ID": "BadNetCode.PonyWeightLossProgram", "Default | UpdateKey": "Nexus:1232" }, - { - // Portraiture + + "Portraiture": { "ID": "Platonymous.Portraiture", "Default | UpdateKey": "Nexus:999" // added in 1.3.1 }, - { - // Prairie King Made Easy + + "Prairie King Made Easy": { "ID": "Mucchan.PrairieKingMadeEasy", "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 "Default | UpdateKey": "Chucklefish:3594", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Quest Delay + + "Quest Delay": { "ID": "BadNetCode.QuestDelay", "Default | UpdateKey": "Nexus:1239" }, - { - // Rain Randomizer + + "Rain Randomizer": { "ID": "{EntryDll: 'RainRandomizer.dll'}", "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Recatch Legendary Fish + + "Recatch Legendary Fish": { "ID": "cantorsdust.RecatchLegendaryFish", "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 "Default | UpdateKey": "Nexus:172" }, - { - // Regeneration + + "Regeneration": { "ID": "HammurabiRegeneration", "Default | UpdateKey": "Chucklefish:4584" }, - { - // Relationship Bar UI + + "Relationship Bar UI": { "ID": "RelationshipBar", "Default | UpdateKey": "Nexus:1009" }, - { - // RelationshipsEnhanced + + "RelationshipsEnhanced": { "ID": "relationshipsenhanced", "Default | UpdateKey": "Chucklefish:4435", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Relationship Status + + "Relationship Status": { "ID": "relationshipstatus", "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest "Default | UpdateKey": "Nexus:751", "~1.0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Rented Tools + + "Rented Tools": { "ID": "JarvieK.RentedTools", "Default | UpdateKey": "Nexus:1307" }, - { - // Replanter + + "Replanter": { "ID": "jwdred.Replanter", "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 "Default | UpdateKey": "Nexus:589", "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // ReRegeneration + + "ReRegeneration": { "ID": "lrsk_sdvm_rerg.0925160827", "MapLocalVersions": { "1.1.2-release": "1.1.2" }, "Default | UpdateKey": "Chucklefish:4465" }, - { - // Reseed + + "Reseed": { "ID": "Roc.Reseed", "Default | UpdateKey": "Nexus:887" }, - { - // Reusable Wallpapers and Floors (Wallpaper Retain) + + "Reusable Wallpapers and Floors (Wallpaper Retain)": { "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", "Default | UpdateKey": "Nexus:356", "~1.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Ring of Fire + + "Ring of Fire": { "ID": "Platonymous.RingOfFire", "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 }, - { - // Rope Bridge + + "Rope Bridge": { "ID": "RopeBridge", "Default | UpdateKey": "Nexus:824" }, - { - // Rotate Toolbar + + "Rotate Toolbar": { "ID": "Pathoschild.RotateToolbar", "Default | UpdateKey": "Nexus:1100" }, - { - // Rush Orders + + "Rush Orders": { "ID": "spacechase0.RushOrders", "FormerIDs": "RushOrders", // changed in 1.1 "Default | UpdateKey": "Nexus:605", "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Save Anywhere + + "Save Anywhere": { "ID": "Omegasis.SaveAnywhere", "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl "Default | UpdateKey": "Nexus:444", // added in 2.6.1 "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Save Backup + + "Save Backup": { "ID": "Omegasis.SaveBackup", "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods "Default | UpdateKey": "Nexus:435", // added in 1.3.1 "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Scroll to Blank + + "Scroll to Blank": { "ID": "caraxian.scroll.to.blank", "Default | UpdateKey": "Chucklefish:4405" }, - { - // Scythe Harvesting + + "Scythe Harvesting": { "ID": "mmanlapat.ScytheHarvesting", "FormerIDs": "ScytheHarvesting", // changed in 1.6 "Default | UpdateKey": "Nexus:1106" }, - { - // Seasonal Immersion + + "Seasonal Immersion": { "ID": "Entoarox.SeasonalImmersion", "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Seed Bag + + "Seed Bag": { "ID": "Platonymous.SeedBag", "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 }, - { - // Self Service + + "Self Service": { "ID": "JarvieK.SelfService", "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated "Default | UpdateKey": "Nexus:1304" }, - { - // Send Items + + "Send Items": { "ID": "Denifia.SendItems", "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Shed Notifications (BuildingsNotifications) + + "Shed Notifications (BuildingsNotifications)": { "ID": "TheCroak.BuildingsNotifications", "Default | UpdateKey": "Nexus:620", "~0.4.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~0.4.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Shenandoah Project + + "Shenandoah Project": { "ID": "Shenandoah Project", "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest "Default | UpdateKey": "Nexus:756", "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Ship Anywhere + + "Ship Anywhere": { "ID": "spacechase0.ShipAnywhere", "Default | UpdateKey": "Nexus:1379" }, - { - // Shipment Tracker + + "Shipment Tracker": { "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", "Default | UpdateKey": "Nexus:321", "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Shop Expander + + "Shop Expander": { "ID": "Entoarox.ShopExpander", "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths "~1.5.3 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.5.3 by request (has its own update-check feature) "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Showcase Mod + + "Showcase Mod": { "ID": "Igorious.Showcase", "MapLocalVersions": { "0.9-500": "0.9" }, "Default | UpdateKey": "Chucklefish:4487", "~0.9 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~0.9 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Shroom Spotter + + "Shroom Spotter": { "ID": "TehPers.ShroomSpotter", "Default | UpdateKey": "Nexus:908" }, - { - // Simple Crop Label + + "Simple Crop Label": { "ID": "SimpleCropLabel", "Default | UpdateKey": "Nexus:314" }, - { - // Simple Sound Manager + + "Simple Sound Manager": { "ID": "Omegasis.SimpleSoundManager", "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Simple Sprinklers + + "Simple Sprinklers": { "ID": "tZed.SimpleSprinkler", "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 "Default | UpdateKey": "Nexus:76", "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Siv's Marriage Mod + + "Siv's Marriage Mod": { "ID": "6266959802", "MapLocalVersions": { "0.0": "1.4" }, "Default | UpdateKey": "Nexus:366", "~1.2.2 | Status": "AssumeBroken", // broke in SMAPI 1.9 (has multiple Mod instances) "~1.2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Skill Prestige + + "Skill Prestige": { "ID": "alphablackwolf.skillPrestige", "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 "Default | UpdateKey": "Nexus:569", "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Skill Prestige: Cooking Adapter + + "Skill Prestige: Cooking Adapter": { "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated "Default | UpdateKey": "Nexus:569", "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Skip Intro + + "Skip Intro": { "ID": "Pathoschild.SkipIntro", "FormerIDs": "SkipIntro", // changed in 1.4 "Default | UpdateKey": "Nexus:533" }, - { - // Skull Cavern Elevator + + "Skull Cavern Elevator": { "ID": "SkullCavernElevator", "Default | UpdateKey": "Nexus:963" }, - { - // Skull Cave Saver + + "Skull Cave Saver": { "ID": "cantorsdust.SkullCaveSaver", "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 "Default | UpdateKey": "Nexus:175" }, - { - // Sleepy Eye + + "Sleepy Eye": { "ID": "spacechase0.SleepyEye", "Default | UpdateKey": "Nexus:1152" }, - { - // Slower Fence Decay + + "Slower Fence Decay": { "ID": "Speeder.SlowerFenceDecay", "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update "Default | UpdateKey": "Nexus:252", "~0.5.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~0.5.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Smart Mod + + "Smart Mod": { "ID": "KuroBear.SmartMod", "~2.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Solar Eclipse Event + + "Solar Eclipse Event": { "ID": "KoihimeNakamura.SolarEclipseEvent", "Default | UpdateKey": "Nexus:897", "MapLocalVersions": { "1.3-20170917": "1.3" }, "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // SpaceCore + + "SpaceCore": { "ID": "spacechase0.SpaceCore", "Default | UpdateKey": "Nexus:1348" }, - { - // Speedster + + "Speedster": { "ID": "Platonymous.Speedster", "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 }, - { - // Sprinkler Range + + "Sprinkler Range": { "ID": "cat.sprinklerrange", "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated "Default | UpdateKey": "Nexus:1179" }, - { - // Sprinkles + + "Sprinkles": { "ID": "Platonymous.Sprinkles", "Default | UpdateKey": "Chucklefish:4592", "~1.1.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Sprint and Dash + + "Sprint and Dash": { "ID": "SPDSprintAndDash", "Default | UpdateKey": "Chucklefish:3531", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Sprint and Dash Redux + + "Sprint and Dash Redux": { "ID": "littleraskol.SprintAndDashRedux", "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 "Default | UpdateKey": "Chucklefish:4201" }, - { - // Sprinting Mod + + "Sprinting Mod": { "ID": "a10d3097-b073-4185-98ba-76b586cba00c", "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", "~2.1 | Status": "AssumeBroken", // broke in SDV 1.2 "~2.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // StackSplitX + + "StackSplitX": { "ID": "tstaples.StackSplitX", "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 "Default | UpdateKey": "Nexus:798", "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // StaminaRegen + + "StaminaRegen": { "ID": "{EntryDll: 'StaminaRegen.dll'}", "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Stardew Config Menu + + "Stardew Config Menu": { "ID": "Juice805.StardewConfigMenu", "Default | UpdateKey": "Nexus:1312" }, - { - // Stardew Content Compatibility Layer (SCCL) + + "Stardew Content Compatibility Layer (SCCL)": { "ID": "SCCL", "Default | UpdateKey": "Nexus:889", "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Stardew Editor Game Integration + + "Stardew Editor Game Integration": { "ID": "spacechase0.StardewEditor.GameIntegration", "Default | UpdateKey": "Nexus:1298" }, - { - // Stardew Notification + + "Stardew Notification": { "ID": "stardewnotification", "Default | UpdateKey": "GitHub:monopandora/StardewNotification", "~1.7 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.7 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Stardew Symphony + + "Stardew Symphony": { "ID": "Omegasis.StardewSymphony", "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:425", // added in 1.4.1 "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // StarDustCore + + "StarDustCore": { "ID": "StarDustCore", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." }, - { - // Starting Money + + "Starting Money": { "ID": "mmanlapat.StartingMoney", "FormerIDs": "StartingMoney", // changed in 1.1 "Default | UpdateKey": "Nexus:1138" }, - { - // StashItemsToChest + + "StashItemsToChest": { "ID": "BlueMod_StashItemsToChest", "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", "~1.0.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Stephan's Lots of Crops + + "Stephan's Lots of Crops": { "ID": "stephansstardewcrops", "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated "Default | UpdateKey": "Chucklefish:4314" }, - { - // Stone Bridge Over Pond (PondWithBridge) + + "Stone Bridge Over Pond (PondWithBridge)": { "ID": "{EntryDll: 'PondWithBridge.dll'}", "MapLocalVersions": { "0.0": "1.0" }, "Default | UpdateKey": "Nexus:316", "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Stumps to Hardwood Stumps + + "Stumps to Hardwood Stumps": { "ID": "StumpsToHardwoodStumps", "Default | UpdateKey": "Nexus:691" }, - { - // Super Greenhouse Warp Modifier + + "Super Greenhouse Warp Modifier": { "ID": "SuperGreenhouse", "Default | UpdateKey": "Chucklefish:4334", "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Swim Almost Anywhere / Swim Suit + + "Swim Almost Anywhere / Swim Suit": { "ID": "Platonymous.SwimSuit", "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 }, - { - // Tainted Cellar + + "Tainted Cellar": { "ID": "{EntryDll: 'TaintedCellar.dll'}", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Tapper Ready + + "Tapper Ready": { "ID": "skunkkk.TapperReady", "Default | UpdateKey": "Nexus:1219" }, - { - // Teh's Fishing Overhaul + + "Teh's Fishing Overhaul": { "ID": "TehPers.FishingOverhaul", "Default | UpdateKey": "Nexus:866" }, - { - // Teleporter + + "Teleporter": { "ID": "Teleporter", "Default | UpdateKey": "Chucklefish:4374", "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // The Long Night + + "The Long Night": { "ID": "Pathoschild.TheLongNight", "Default | UpdateKey": "Nexus:1369" }, - { - // Three-heart Dance Partner + + "Three-heart Dance Partner": { "ID": "ThreeHeartDancePartner", "Default | UpdateKey": "Nexus:500", "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // TimeFreeze + + "TimeFreeze": { "ID": "Omegasis.TimeFreeze", "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 "Default | UpdateKey": "Nexus:973" // added in 1.2.1 }, - { - // Time Reminder + + "Time Reminder": { "ID": "KoihimeNakamura.TimeReminder", "MapLocalVersions": { "1.0-20170314": "1.0.2" }, "Default | UpdateKey": "Nexus:1000" }, - { - // TimeSpeed + + "TimeSpeed": { "ID": "cantorsdust.TimeSpeed", "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis "Default | UpdateKey": "Nexus:169", "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // TractorMod + + "TractorMod": { "ID": "Pathoschild.TractorMod", "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 "Default | UpdateKey": "Nexus:1401" }, - { - // TrainerMod + + "TrainerMod": { "ID": "SMAPI.TrainerMod", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." }, - { - // Tree Transplant + + "Tree Transplant": { "ID": "TreeTransplant", "Default | UpdateKey": "Nexus:1342" }, - { - // UI Info Suite + + "UI Info Suite": { "ID": "Cdaragorn.UiInfoSuite", "Default | UpdateKey": "Nexus:1150" }, - { - // UiModSuite + + "UiModSuite": { "ID": "Demiacle.UiModSuite", "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest "Default | UpdateKey": "Nexus:1023", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Variable Grass + + "Variable Grass": { "ID": "dantheman999.VariableGrass", "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" }, - { - // Vertical Toolbar + + "Vertical Toolbar": { "ID": "SB_VerticalToolMenu", "Default | UpdateKey": "Nexus:943" }, - { - // WakeUp + + "WakeUp": { "ID": "{EntryDll: 'WakeUp.dll'}", "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Wallpaper Fix + + "Wallpaper Fix": { "ID": "{EntryDll: 'WallpaperFix.dll'}", "Default | UpdateKey": "Chucklefish:4211", "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // WarpAnimals + + "WarpAnimals": { "ID": "Symen.WarpAnimals", "Default | UpdateKey": "Nexus:1400" }, - { - // Weather Controller + + "Weather Controller": { "ID": "{EntryDll: 'WeatherController.dll'}", "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // What Farm Cave / WhatAMush + + "What Farm Cave / WhatAMush": { "ID": "WhatAMush", "Default | UpdateKey": "Nexus:1097" }, - { - // WHats Up + + "WHats Up": { "ID": "wHatsUp", "Default | UpdateKey": "Nexus:1082" }, - { - // Wonderful Farm Life + + "Wonderful Farm Life": { "ID": "{EntryDll: 'WonderfulFarmLife.dll'}", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // XmlSerializerRetool + + "XmlSerializerRetool": { "ID": "{EntryDll: 'XmlSerializerRetool.dll'}", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it's no longer maintained or used." }, - { - // Xnb Loader + + "Xnb Loader": { "ID": "Entoarox.XnbLoader", "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // zDailyIncrease + + "zDailyIncrease": { "ID": "zdailyincrease", "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest "Default | UpdateKey": "Chucklefish:4247", "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoom Out Extreme + + "Zoom Out Extreme": { "ID": "RockinMods.ZoomMod", "FormerIDs": "ZoomMod", // changed circa 1.2.1 "Default | UpdateKey": "Nexus:1326", "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoryn's Better RNG + + "Zoryn's Better RNG": { "ID": "Zoryn.BetterRNG", "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoryn's Calendar Anywhere + + "Zoryn's Calendar Anywhere": { "ID": "Zoryn.CalendarAnywhere", "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoryn's Durable Fences + + "Zoryn's Durable Fences": { "ID": "Zoryn.DurableFences", "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" }, - { - // Zoryn's Health Bars + + "Zoryn's Health Bars": { "ID": "Zoryn.HealthBars", "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoryn's Fishing Mod + + "Zoryn's Fishing Mod": { "ID": "Zoryn.FishingMod", "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" }, - { - // Zoryn's Junimo Deposit Anywhere + + "Zoryn's Junimo Deposit Anywhere": { "ID": "Zoryn.JunimoDepositAnywhere", "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoryn's Movement Mod + + "Zoryn's Movement Mod": { "ID": "Zoryn.MovementModifier", "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Zoryn's Regen Mod + + "Zoryn's Regen Mod": { "ID": "Zoryn.RegenMod", "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 } - ] + } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 129c88b0..e181c435 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -89,9 +89,10 @@ - - - + + + + @@ -176,7 +177,7 @@ - + @@ -204,7 +205,7 @@ - + -- cgit From 4444b590f016ebecfc113a0dd4584723b0250f41 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 16:34:31 -0500 Subject: add content pack feature (#436) --- src/SMAPI/Framework/ContentPack.cs | 78 +++++++++++ src/SMAPI/Framework/IModMetadata.cs | 18 ++- src/SMAPI/Framework/InternalExtensions.cs | 2 +- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 19 ++- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 29 +++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 151 +++++++++++++++------ src/SMAPI/Framework/ModRegistry.cs | 17 ++- src/SMAPI/Framework/Models/Manifest.cs | 6 +- .../Framework/Models/ManifestContentPackFor.cs | 15 ++ .../ManifestContentPackForConverter.cs | 50 +++++++ src/SMAPI/IContentPack.cs | 42 ++++++ src/SMAPI/IManifest.cs | 5 +- src/SMAPI/IManifestContentPackFor.cs | 12 ++ src/SMAPI/IModHelper.cs | 12 +- src/SMAPI/Program.cs | 90 ++++++++++-- src/SMAPI/StardewModdingAPI.csproj | 7 +- 16 files changed, 485 insertions(+), 68 deletions(-) create mode 100644 src/SMAPI/Framework/ContentPack.cs create mode 100644 src/SMAPI/Framework/Models/ManifestContentPackFor.cs create mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs create mode 100644 src/SMAPI/IContentPack.cs create mode 100644 src/SMAPI/IManifestContentPackFor.cs (limited to 'src/SMAPI/Framework/Models') diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs new file mode 100644 index 00000000..0a8f223e --- /dev/null +++ b/src/SMAPI/Framework/ContentPack.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Serialisation; +using xTile; + +namespace StardewModdingAPI.Framework +{ + /// Manages access to a content pack's metadata and files. + internal class ContentPack : IContentPack + { + /********* + ** Properties + *********/ + /// Provides an API for loading content assets. + private readonly IContentHelper Content; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + + /********* + ** Accessors + *********/ + /// The full path to the content pack's folder. + public string DirectoryPath { get; } + + /// The content pack's manifest. + public IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full path to the content pack's folder. + /// The content pack's manifest. + /// Provides an API for loading content assets. + /// Encapsulates SMAPI's JSON file parsing. + public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, JsonHelper jsonHelper) + { + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Content = content; + this.JsonHelper = jsonHelper; + } + + /// Read a JSON file from the content pack folder. + /// The model type. + /// The file path relative to the contnet directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + public TModel ReadJsonFile(string path) where TModel : class + { + path = Path.Combine(this.DirectoryPath, path); + return this.JsonHelper.ReadJsonFile(path); + } + + /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T LoadAsset(string key) + { + return this.Content.Load(key, ContentSource.ModFolder); + } + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The the local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + public string GetActualAssetKey(string key) + { + return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); + } + + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index a91b0a5b..d1e8eb7d 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework /// The mod manifest. IManifest Manifest { get; } - /// >Metadata about the mod from SMAPI's internal data (if any). + /// Metadata about the mod from SMAPI's internal data (if any). ParsedModDataRecord DataRecord { get; } /// The metadata resolution status. @@ -27,12 +27,21 @@ namespace StardewModdingAPI.Framework /// The reason the metadata is invalid, if any. string Error { get; } - /// The mod instance (if it was loaded). + /// The mod instance (if loaded and is false). IMod Mod { get; } + /// The content pack instance (if loaded and is true). + IContentPack ContentPack { get; } + + /// Writes messages to the console and log file as this mod. + IMonitor Monitor { get; } + /// The mod-provided API (if any). object Api { get; } + /// Whether the mod is a content pack. + bool IsContentPack { get; } + /********* ** Public methods @@ -47,6 +56,11 @@ namespace StardewModdingAPI.Framework /// The mod instance to set. IModMetadata SetMod(IMod mod); + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + IModMetadata SetMod(IContentPack contentPack, IMonitor monitor); + /// Set the mod-provided API instance. /// The mod-provided API. IModMetadata SetApi(object api); diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 0340a92d..71489627 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) { - metadata.Mod.Monitor.Log(message, level); + metadata.Monitor.Log(message, level); } /**** diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 665b9cf4..c73dc307 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,5 +1,7 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.ModHelpers @@ -13,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// The content packs loaded for this mod. + private readonly IContentPack[] ContentPacks; + /********* ** Accessors @@ -48,9 +53,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /// an API for fetching metadata about loaded mods. /// An API for accessing private game code. /// An API for reading translations stored in the mod's i18n folder. + /// The content packs loaded for this mod. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable contentPacks) : base(modID) { // validate directory @@ -67,6 +73,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); + this.ContentPacks = contentPacks.ToArray(); } /**** @@ -116,6 +123,14 @@ namespace StardewModdingAPI.Framework.ModHelpers this.JsonHelper.WriteJsonFile(path, model); } + /**** + ** Content packs + ****/ + /// Get all content packs loaded for this mod. + public IEnumerable GetContentPacks() + { + return this.ContentPacks; + } /**** ** Disposal diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 29bb6617..1a0f9994 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading @@ -26,12 +27,21 @@ namespace StardewModdingAPI.Framework.ModLoading /// The reason the metadata is invalid, if any. public string Error { get; private set; } - /// The mod instance (if it was loaded). + /// The mod instance (if loaded and is false). public IMod Mod { get; private set; } + /// The content pack instance (if loaded and is true). + public IContentPack ContentPack { get; private set; } + + /// Writes messages to the console and log file as this mod. + public IMonitor Monitor { get; private set; } + /// The mod-provided API (if any). public object Api { get; private set; } + /// Whether the mod is a content pack. + public bool IsContentPack => this.Manifest?.ContentPackFor != null; + /********* ** Public methods @@ -64,7 +74,24 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod instance to set. public IModMetadata SetMod(IMod mod) { + if (this.ContentPack != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + this.Mod = mod; + this.Monitor = mod.Monitor; + return this; + } + + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor) + { + if (this.Mod != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + + this.ContentPack = contentPack; + this.Monitor = monitor; return this; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b46ee117..be73254d 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -30,18 +30,13 @@ namespace StardewModdingAPI.Framework.ModLoading string error = null; try { - // read manifest manifest = jsonHelper.ReadJsonFile(path); - - // validate if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - error = "its manifest doesn't set an entry DLL."; } catch (SParseException ex) { @@ -85,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; - // validate compatibility + // validate compatibility from internal data switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: @@ -128,24 +123,52 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL value - if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll)) - { - mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field."); - continue; - } - if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + // validate DLL / content pack fields { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } + bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); + bool isContentPack = mod.Manifest.ContentPackFor != null; - // validate DLL path - string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; + // validate field presence + if (!hasDll && !isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + continue; + } + if (hasDll && isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + continue; + } + + // validate DLL + if (hasDll) + { + // invalid filename format + if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + continue; + } + + // invalid path + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); + if (!File.Exists(assemblyPath)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + } + + // validate content pack + else + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + continue; + } + } } // validate required fields @@ -243,30 +266,17 @@ namespace StardewModdingAPI.Framework.ModLoading throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } - // no dependencies, mark sorted - if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) + // collect dependencies + ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + + // mark sorted if no dependencies + if (!dependencies.Any()) { sortedMods.Push(mod); return states[mod] = ModDependencyStatus.Sorted; } - // get dependencies - var dependencies = - ( - 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, - IsRequired = entry.IsRequired - } - ) - .ToArray(); - - // missing required dependencies, mark failed + // mark failed if missing dependencies { string[] failedModNames = ( from entry in dependencies @@ -371,5 +381,64 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } + + /// Get the dependencies declared in a manifest. + /// The mod manifest. + /// The loaded mods. + private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + { + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + + // yield dependencies + if (manifest.Dependencies != null) + { + foreach (var entry in manifest.Dependencies) + yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired); + } + + // yield content pack parent + if (manifest.ContentPackFor != null) + yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true); + } + + + /********* + ** Private models + *********/ + /// Represents a dependency from one mod to another. + private struct ModDependency + { + /********* + ** Accessors + *********/ + /// The unique ID of the required mod. + public string ID { get; } + + /// The minimum required version (if any). + public ISemanticVersion MinVersion { get; } + + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public bool IsRequired { get; } + + /// The loaded mod that fulfills the dependency (if available). + public IModMetadata Mod { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the required mod. + /// The minimum required version (if any). + /// The loaded mod that fulfills the dependency (if available). + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired) + { + this.ID = id; + this.MinVersion = minVersion; + this.Mod = mod; + this.IsRequired = isRequired; + } + } } } diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 453d2868..e7d4f89a 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -25,18 +25,27 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ - /// Register a mod as a possible source of deprecation warnings. + /// Register a mod. /// The mod metadata. public void Add(IModMetadata metadata) { this.Mods.Add(metadata); - this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; + if (!metadata.IsContentPack) + this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; } /// Get metadata for all loaded mods. - public IEnumerable GetAll() + /// Whether to include SMAPI mods. + /// Whether to include content pack mods. + public IEnumerable GetAll(bool assemblyMods = true, bool contentPacks = true) { - return this.Mods.Select(p => p); + IEnumerable query = this.Mods; + if (!assemblyMods) + query = query.Where(p => p.IsContentPack); + if (!contentPacks) + query = query.Where(p => !p.IsContentPack); + + return query; } /// Get metadata for a loaded mod. diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index f9762406..74303cba 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -27,9 +27,13 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. Mutually exclusive with . public string EntryDll { get; set; } + /// The mod which will read this as a content pack. Mutually exclusive with . + [JsonConverter(typeof(ManifestContentPackForConverter))] + public IManifestContentPackFor ContentPackFor { get; set; } + /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestDependencyArrayConverter))] public IManifestDependency[] Dependencies { get; set; } diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..7836bbcc --- /dev/null +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + internal class ManifestContentPackFor : IManifestContentPackFor + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod which can read this content pack. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs new file mode 100644 index 00000000..af7558f6 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters +{ + /// Handles deserialisation of arrays. + internal class ManifestContentPackForConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IManifestContentPackFor[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs new file mode 100644 index 00000000..15a2b7dd --- /dev/null +++ b/src/SMAPI/IContentPack.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using xTile; + +namespace StardewModdingAPI +{ + /// An API that provides access to a content pack. + public interface IContentPack + { + /********* + ** Accessors + *********/ + /// The full path to the content pack's folder. + string DirectoryPath { get; } + + /// The content pack's manifest. + IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// Read a JSON file from the content pack folder. + /// The model type. + /// The file path relative to the content pack directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + TModel ReadJsonFile(string path) where TModel : class; + + /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T LoadAsset(string key); + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The the local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + string GetActualAssetKey(string key); + } +} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs index 9db1d538..183ac105 100644 --- a/src/SMAPI/IManifest.cs +++ b/src/SMAPI/IManifest.cs @@ -26,9 +26,12 @@ namespace StardewModdingAPI /// The unique mod ID. string UniqueID { get; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. Mutually exclusive with . string EntryDll { get; } + /// The mod which will read this as a content pack. Mutually exclusive with . + IManifestContentPackFor ContentPackFor { get; } + /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } diff --git a/src/SMAPI/IManifestContentPackFor.cs b/src/SMAPI/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/SMAPI/IManifestContentPackFor.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public interface IManifestContentPackFor + { + /// The unique ID of the mod which can read this content pack. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + } +} diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 116e8508..96265c85 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,4 +1,6 @@ -namespace StardewModdingAPI +using System.Collections.Generic; + +namespace StardewModdingAPI { /// Provides simplified APIs for writing mods. public interface IModHelper @@ -54,5 +56,11 @@ /// The file path relative to the mod directory. /// The model to save. void WriteJsonFile(string path, TModel model) where TModel : class; + + /**** + ** Content packs + ****/ + /// Get all content packs loaded for this mod. + IEnumerable GetContentPacks(); } -} \ No newline at end of file +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index fd2bb340..e0064714 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -394,7 +394,7 @@ namespace StardewModdingAPI LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); } @@ -652,15 +652,52 @@ namespace StardewModdingAPI { this.Monitor.Log("Loading mods...", LogLevel.Trace); - // load mod assemblies IDictionary skippedMods = new Dictionary(); + void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + + // load content packs + foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) + { + // get basic info + IManifest manifest = metadata.Manifest; + this.Monitor.Log($"Loading {metadata.DisplayName} from {metadata.DirectoryPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)} (content pack)...", LogLevel.Trace); + + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } + + // load mod as content pack + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); + metadata.SetMod(contentPack, monitor); + this.ModRegistry.Add(metadata); + } + IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); + + // load mods { - void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + // get content packs by mod ID + IDictionary contentPacksByModID = + loadedContentPacks + .GroupBy(p => p.Manifest.ContentPackFor.UniqueID) + .ToDictionary( + group => group.Key, + group => group.Select(metadata => metadata.ContentPack).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + // get assembly loaders AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - foreach (IModMetadata metadata in mods) + + // load from metadata + foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) { // get basic info IManifest manifest = metadata.Manifest; @@ -676,7 +713,7 @@ namespace StardewModdingAPI continue; } - // preprocess & load mod assembly + // load mod string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) : null; @@ -704,6 +741,10 @@ namespace StardewModdingAPI // initialise mod try { + // get content packs + if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) + contentPacks = new IContentPack[0]; + // init mod helpers IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IModHelper modHelper; @@ -713,7 +754,7 @@ namespace StardewModdingAPI IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks); } // get mod instance @@ -735,7 +776,7 @@ namespace StardewModdingAPI } } } - IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); // log skipped mods this.Monitor.Newline(); @@ -757,6 +798,7 @@ namespace StardewModdingAPI // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; @@ -769,10 +811,30 @@ namespace StardewModdingAPI } this.Monitor.Newline(); + // log loaded content packs + if (loadedContentPacks.Any()) + { + string GetModDisplayName(string id) => loadedMods.First(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + + this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); + foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (metadata.IsContentPack ? $" | content pack for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + } + // initialise translations - this.ReloadTranslations(); + this.ReloadTranslations(loadedMods); - // initialise loaded mods + // initialise loaded non-content-pack mods foreach (IModMetadata metadata in loadedMods) { // add interceptors @@ -891,11 +953,15 @@ namespace StardewModdingAPI } /// Reload translations for all mods. - private void ReloadTranslations() + /// The mods for which to reload translations. + private void ReloadTranslations(IEnumerable mods) { JsonHelper jsonHelper = new JsonHelper(); - foreach (IModMetadata metadata in this.ModRegistry.GetAll()) + foreach (IModMetadata metadata in mods) { + if (metadata.IsContentPack) + throw new InvalidOperationException("Can't reload translations for a content pack."); + // read translation files IDictionary> translations = new Dictionary>(); DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); @@ -954,7 +1020,7 @@ namespace StardewModdingAPI break; case "reload_i18n": - this.ReloadTranslations(); + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index eb403309..7cf62a91 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -93,6 +94,7 @@ + @@ -114,6 +116,7 @@ + @@ -121,6 +124,8 @@ + + @@ -279,4 +284,4 @@ - \ No newline at end of file + -- cgit From f1c24e30522499199cbf2f75cb68d7b4e5942bf3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Feb 2018 02:31:39 -0500 Subject: add support for ISemanticVersion in JSON models --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/Manifest.cs | 2 -- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework/Models') diff --git a/docs/release-notes.md b/docs/release-notes.md index f0a7a718..a1fb4a9d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Updated compatibility list and enabled update checks for more old mods. * For modders: + * Added support for `ISemanticVersion` in JSON models. * Fixed deadlock in rare cases when injecting a file with an asset loader. * Fixed unhelpful error when a mod exposes a non-public API. diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index 74303cba..f5867cf3 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -20,11 +20,9 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// The mod version. - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the method. Mutually exclusive with . diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 2e2a666e..6cba343e 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -21,6 +21,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { + // SMAPI types + new SemanticVersionConverter(), + // enums new StringEnumConverter(), new StringEnumConverter(), -- cgit