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) --- 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 +- 18 files changed, 1088 insertions(+), 1005 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') 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