From 032997650010a9b6cd3378cb1a2b8273fb3f56ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Feb 2018 23:06:44 -0500 Subject: rewrite all mod assemblies to let SMAPI proxy into their internal classes (#435) --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 80 +++++++++++------------- 1 file changed, 38 insertions(+), 42 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 3a7b214a..ac849971 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; @@ -94,23 +95,14 @@ namespace StardewModdingAPI.Framework.ModLoading if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); - if (changed) + this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) { - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } - } - else - { - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); - lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); } } @@ -192,38 +184,48 @@ namespace StardewModdingAPI.Framework.ModLoading /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + private void RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; + // let SMAPI proxy mod internals for mod-provided APIs + { + MethodReference attributeConstructor = module.Import(typeof(InternalsVisibleToAttribute).GetConstructor(new[] { typeof(string) })); + CustomAttribute attribute = new CustomAttribute(attributeConstructor); + attribute.ConstructorArguments.Add(new CustomAttributeArgument(module.TypeSystem.String, "StardewModdingAPI.Proxies")); + assembly.CustomAttributes.Add(attribute); + } + // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + for (int i = 0; i < module.AssemblyReferences.Count; i++) { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); } - } - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); } // find (and optionally rewrite) incompatible instructions - bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { @@ -232,8 +234,6 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; } // check CIL instructions @@ -244,13 +244,9 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; } } } - - return platformChanged || anyRewritten; } /// Process the result from an instruction handler. -- cgit 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. --- docs/release-notes.md | 1 + src/SMAPI.Tests/Core/ModResolverTests.cs | 6 +- src/SMAPI/Framework/IModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 24 +- 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 + .../ModCompatibilityArrayConverter.cs | 61 - .../SmapiConverters/ModDataIdConverter.cs | 19 - src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.config.json | 1731 +++++++++----------- src/SMAPI/StardewModdingAPI.csproj | 7 +- 16 files changed, 1099 insertions(+), 1266 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 delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index c7f5cfe9..dd39a179 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * For SMAPI developers: * All mod assemblies are now rewritten in-memory to support features like mod-provided APIs. + * Overhauled `StardewModdingApi.config.json`'s `ModData` format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. ## 2.4 * For players: diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 051ffe99..7c1efe53 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -141,9 +141,9 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModDataRecord + this.SetupMetadataForValidation(mock, new ParsedModDataRecord { - Compatibility = new[] { new ModCompatibility("~1.0", ModStatus.AssumeBroken, null) }, + Status = ModStatus.AssumeBroken, AlternativeUrl = "http://example.org" }); @@ -544,7 +544,7 @@ namespace StardewModdingAPI.Tests.Core /// Set up a mock mod metadata for . /// The mock mod metadata. /// The extra metadata about the mod from SMAPI's internal data (if any). - private void SetupMetadataForValidation(Mock mod, ModDataRecord modRecord = null) + private void SetupMetadataForValidation(Mock mod, ParsedModDataRecord modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DataRecord).Returns(() => null); diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index a36994fd..41484567 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework IManifest Manifest { get; } /// >Metadata about the mod from SMAPI's internal data (if any). - ModDataRecord DataRecord { get; } + ParsedModDataRecord DataRecord { get; } /// The metadata resolution status. ModMetadataStatus Status { get; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 30fe211b..1a71920e 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// Metadata about the mod from SMAPI's internal data (if any). - public ModDataRecord DataRecord { get; } + public ParsedModDataRecord DataRecord { get; } /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's full directory path. /// The mod manifest. /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9802d9e9..6671e880 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -53,18 +53,15 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get internal data record (if any) - ModDataRecord dataRecord = null; + // parse internal data record (if any) + ParsedModDataRecord dataRecord = null; if (manifest != null) { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); + ModDataRecord rawDataRecord = dataRecords.FirstOrDefault(p => p.Matches(manifest)); + if (rawDataRecord != null) + dataRecord = rawDataRecord.ParseFieldsFor(manifest); } - // add default update keys - if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) - manifest.UpdateKeys = dataRecord.UpdateKeys; - // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name @@ -93,17 +90,16 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // validate compatibility - ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); - switch (compatibility?.Status) + switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List updateUrls = new List(); @@ -124,10 +120,10 @@ namespace StardewModdingAPI.Framework.ModLoading // build error string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) error += "newer version"; else - error += $"version newer than {compatibility.UpperVersion}"; + error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); 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); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs deleted file mode 100644 index 3232dde4..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// Handles deserialisation of arrays. - internal class ModCompatibilityArrayConverter : 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(ModCompatibility[]); - } - - - /********* - ** 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) - { - List result = new List(); - foreach (JProperty property in JObject.Load(reader).Properties()) - { - string range = property.Name; - ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value(nameof(ModCompatibility.Status))); - string reasonPhrase = property.Value.Value(nameof(ModCompatibility.ReasonPhrase)); - - result.Add(new ModCompatibility(range, status, reasonPhrase)); - } - return result.ToArray(); - } - - /// 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/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs deleted file mode 100644 index 8a10db47..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// Handles deserialisation of . - internal class ModDataIdConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override ModDataID ReadString(string str, string path) - { - return new ModDataID(str); - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c7da581d..b5ce3033 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -679,7 +679,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 18a9f978..ca0c20b1 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -39,2041 +39,1770 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "VerboseLogging": false, /** - * Extra metadata about some SMAPI mods. All fields except 'ID' are optional. + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. * - * - 'ID' uniquely identifies the mod across all versions, even if its manifest fields changed or - * the mod doesn't have a unique ID. The format is as follows: - * - If the mod's identifier changed over time, multiple variants are separated by |. - * - Each variant can take one of two forms: a simple string matching the mod's UniqueID, - * or a JSON structure containing any of three manifest fields (ID, Name, and Author) to - * match. + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). * - * - 'UpdateKeys' specifies the value of the equivalent manifest field if it's not already set. - * This is used to enable update checks for older mods that haven't been updated to use it yet. + * - ID: the mod's latest unique ID (if any). * - * - 'AlternativeUrl' specifies a URL where the player can find an unofficial update or - * alternative if the mod is no longer compatible. + * - FormerIDs: uniquely identifies the 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. * - * - 'Compatibility' overrides SMAPI's normal compatibility detection. The keys are version - * ranges in the form lower~upper, where either side can be blank for an unbounded range. (For - * example, "~1.0" means all versions up to 1.0 inclusively.) The values have two fields: - * - 'Status' specifies the compatibility. Valid values are Obsolete (SMAPI won't load it - * because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will load it even - * if it detects incompatible code). - * - 'ReasonPhrase' (optional) specifies a message to show to the player explaining why the - * mod isn't loaded. This has no effect for AssumeCompatible. + * - MapLocalVersions and MapRemoteVersions crrect local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. * - * - 'MapLocalVersions' and 'MapRemoteVersions' substitute versions for update checks. For - * example, if the API returns version '1.1-1078', MapRemoteVersions can map it to '1.1' when - * comparing to the mod's current version. This is only intended to support legacy mods with - * injected update keys. + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. */ "ModData": [ { // AccessChestAnywhere "ID": "AccessChestAnywhere", - "UpdateKeys": [ "Nexus:257" ], - "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 - }, - "MapLocalVersions": { - "1.1-1078": "1.1" - } + "MapLocalVersions": { "1.1-1078": "1.1" }, + "Default | UpdateKey": "Nexus:257", + "~1.1 | Status": "AssumeBroken", + "~1.1 | AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // AdjustArtisanPrices "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", - "UpdateKeys": [ "Chucklefish:3532" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 - } + "Default | UpdateKey": "Chucklefish:3532", + "~0.1 | Status": "AssumeBroken", + "~0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Adjust Monster "ID": "mmanlapat.AdjustMonster", - "UpdateKeys": [ "Nexus:1161" ] + "Default | UpdateKey": "Nexus:1161" }, { // Advanced Location Loader "ID": "Entoarox.AdvancedLocationLoader", - //"UpdateKeys": [ "Chucklefish:3619" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + //"Default | UpdateKey": "Chucklefish:3619", // Entoarox opted out of mod update checks + "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Adventure Shop Inventory "ID": "HammurabiAdventureShopInventory", - "UpdateKeys": [ "Chucklefish:4608" ] + "Default | UpdateKey": "Chucklefish:4608" }, { // AgingMod "ID": "skn.AgingMod", - "UpdateKeys": [ "Nexus:1129" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 - "UpdateKeys": [ "Nexus:170" ] + "ID": "community.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7", // changed in 1.3 + "Default | UpdateKey": "Nexus:170" }, { // All Professions - "ID": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 - "UpdateKeys": [ "Nexus:174" ] + "ID": "community.AllProfessions", + "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3", // changed in 1.2 + "Default | UpdateKey": "Nexus:174" }, { // Almighty Tool - "ID": "AlmightyTool.dll | 439", // changed in 1.2.1 - "UpdateKeys": [ "Nexus:439" ], - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapRemoteVersions": { - "1.21": "1.2.1" - } + "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 "ID": "GPeters-AnimalMoodFix", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." }, { // Animal Sitter - "ID": "AnimalSitter.dll | jwdred.AnimalSitter", // changed in 1.0.9 - "UpdateKeys": [ "Nexus:581" ], - "Compatibility": { - "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "UpdateKeys": [ "Nexus:260" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "AaronTaggart.AutoAnimalDoors", - "UpdateKeys": [ "Nexus:1019" ], - "MapRemoteVersions": { - "1.1.1": "1.1" // manifest not updated - } + "MapRemoteVersions": { "1.1.1": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1019" }, { // Auto-Eat - "ID": "BALANCEMOD_AutoEat | Permamiss.AutoEat", // changed in 1.1.1 - "UpdateKeys": [ "Nexus:643" ] + "ID": "Permamiss.AutoEat", + "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 + "Default | UpdateKey": "Nexus:643" }, { // AutoGate "ID": "AutoGate", - "UpdateKeys": [ "Nexus:820" ] + "Default | UpdateKey": "Nexus:820" }, { // Automate "ID": "Pathoschild.Automate", - "UpdateKeys": [ "Nexus:1063" ] + "Default | UpdateKey": "Nexus:1063" }, { // Automated Doors - "ID": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b | azah.automated-doors", // changed in 1.4.1 - "UpdateKeys": [ "GitHub:azah/AutomatedDoors" ], - "MapLocalVersions": { - "1.4.1-1": "1.4.1" - } + "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 - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'} | Omegasis.AutoSpeed", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:443" ] // added in 1.4.1 + "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 "ID": "lrsk_sdvm_bsi.0117171308", - "UpdateKeys": [ "Nexus:833" ], - "MapRemoteVersions": { - "1.0.2": "1.0.1-release" // manifest not updated - } + "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:833" }, { // Better Hay "ID": "cat.betterhay", - "UpdateKeys": [ "Nexus:1430" ] + "Default | UpdateKey": "Nexus:1430" }, { // Better Quality More Seasons "ID": "SB_BQMS", - "UpdateKeys": [ "Nexus:935" ] + "Default | UpdateKey": "Nexus:935" }, { // Better Quarry "ID": "BetterQuarry", - "UpdateKeys": [ "Nexus:771" ] + "Default | UpdateKey": "Nexus:771" }, { // Better Ranching "ID": "BetterRanching", - "UpdateKeys": [ "Nexus:859" ] + "Default | UpdateKey": "Nexus:859" }, { // Better Shipping Box "ID": "Kithio:BetterShippingBox", - "UpdateKeys": [ "Chucklefish:4302" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.0.1": "1.0.2" - } + "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 - "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 - "UpdateKeys": [ "Nexus:41" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'} | Omegasis.BillboardAnywhere", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:492" ] // added in 1.4.1 + "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 - "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d | KathrynHazuka.BirthdayMail", // changed in 1.2.3-pathoschild-update - "UpdateKeys": [ "Nexus:276" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "dycedarger.breedlikerabbits", - "UpdateKeys": [ "Nexus:948" ] + "Default | UpdateKey": "Nexus:948" }, { // Build Endurance - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'} | Omegasis.BuildEndurance", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:445" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'} | Omegasis.BuildHealth", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:446" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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", - "UpdateKeys": [ "Nexus:1538" ] + "Default | UpdateKey": "Nexus:1538" }, { // Buy Cooking Recipes "ID": "Denifia.BuyRecipes", - "UpdateKeys": [ "Nexus:1126" ], // added in 1.0.1 (2017-10-04) - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Buy Back Collectables - "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 - "UpdateKeys": [ "Nexus:507" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "spacechase0.CarryChest", - "UpdateKeys": [ "Nexus:1333" ] + "Default | UpdateKey": "Nexus:1333" }, { // Casks Anywhere "ID": "CasksAnywhere", - "UpdateKeys": [ "Nexus:878" ], - "MapLocalVersions": { - "1.1-alpha": "1.1" - } + "MapLocalVersions": { "1.1-alpha": "1.1" }, + "Default | UpdateKey": "Nexus:878" }, { // Categorize Chests "ID": "CategorizeChests", - "UpdateKeys": [ "Nexus:1300" ] + "Default | UpdateKey": "Nexus:1300" }, { // ChefsCloset "ID": "Duder.ChefsCloset", - "UpdateKeys": [ "Nexus:1030" ], - "MapLocalVersions": { - "1.3-1": "1.3" - } + "MapLocalVersions": { "1.3-1": "1.3" }, + "Default | UpdateKey": "Nexus:1030" }, { // Chest Label System - "ID": "SPDChestLabel | Speeder.ChestLabel", // changed in 1.5.1-pathoschild-update - "UpdateKeys": [ "Nexus:242" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } + "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 - "ID": "ChestPooling.dll | mralbobo.ChestPooling", // changed in 1.3 - "UpdateKeys": [ "GitHub:mralbobo/stardew-chest-pooling" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 - "UpdateKeys": [ "Nexus:518" ], - "Compatibility": { - "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "ChooseBabyGender.dll", - "UpdateKeys": [ "Nexus:590" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "CJBAutomation", - "UpdateKeys": [ "Nexus:211" ], - "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "CJBCheatsMenu | CJBok.CheatsMenu", // changed in 1.14 - "UpdateKeys": [ "Nexus:4" ], - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } + "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 - "ID": "CJBItemSpawner | CJBok.ItemSpawner", // changed in 1.7 - "UpdateKeys": [ "Nexus:93" ], - "Compatibility": { - "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } + "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 - "ID": "CJBShowItemSellPrice | CJBok.ShowItemSellPrice", // changed in 1.7 - "UpdateKeys": [ "Nexus:5" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "ID": "CJBok.ShowItemSellPrice", + "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 + "Default | UpdateKey": "Nexus:5", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, { // Clean Farm "ID": "tstaples.CleanFarm", - "UpdateKeys": [ "Nexus:794" ] + "Default | UpdateKey": "Nexus:794" }, { // Climates of Ferngill "ID": "KoihimeNakamura.ClimatesOfFerngill", - "UpdateKeys": [ "Nexus:604" ], - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:604", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Cold Weather Haley "ID": "LordXamon.ColdWeatherHaleyPRO", - "UpdateKeys": [ "Nexus:1169" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." }, { // Combat with Farm Implements "ID": "SPDFarmingImplementsInCombat", - "UpdateKeys": [ "Nexus:313" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "musbah.bundleTooltip", - "UpdateKeys": [ "Nexus:1329" ] + "Default | UpdateKey": "Nexus:1329" }, { // Concentration on Farming "ID": "punyo.ConcentrationOnFarming", - "UpdateKeys": [ "Nexus:1445" ] + "Default | UpdateKey": "Nexus:1445" }, { // Configurable Machines "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "UpdateKeys": [ "Nexus:280" ], - "MapLocalVersions": { - "1.2-beta": "1.2" - } + "MapLocalVersions": { "1.2-beta": "1.2" }, + "Default | UpdateKey": "Nexus:280" }, { // Configurable Shipping Dates "ID": "ConfigurableShippingDates", - "UpdateKeys": [ "Nexus:675" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 - "UpdateKeys": [ "Nexus:522" ], - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "CrabNet.dll | jwdred.CrabNet", // changed in 1.0.5 - "UpdateKeys": [ "Nexus:584" ], - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "CurrentLocation102120161203", - "UpdateKeys": [ "Nexus:638" ] + "Default | UpdateKey": "Nexus:638" }, { // Custom Critters "ID": "spacechase0.CustomCritters", - "UpdateKeys": [ "Nexus:1255" ] + "Default | UpdateKey": "Nexus:1255" }, { // Custom Element Handler "ID": "Platonymous.CustomElementHandler", - "UpdateKeys": [ "Nexus:1068" ] // added in 1.3.1 + "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 }, { // Custom Farming "ID": "Platonymous.CustomFarming", - "UpdateKeys": [ "Nexus:991" ] // added in 0.6.1 + "Default | UpdateKey": "Nexus:991" // added in 0.6.1 }, { // Custom Farming Automate Bridge "ID": "Platonymous.CFAutomate", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // no longer compatible with Automate - }, - "AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" + "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate + "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" }, { // Custom Farm Types "ID": "spacechase0.CustomFarmTypes", - "UpdateKeys": [ "Nexus:1140" ] + "Default | UpdateKey": "Nexus:1140" }, { // Custom Furniture "ID": "Platonymous.CustomFurniture", - "UpdateKeys": [ "Nexus:1254" ] // added in 0.4.1 + "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 }, { // Customize Exterior - "ID": "CustomizeExterior | spacechase0.CustomizeExterior", // changed in 1.0.3 - "UpdateKeys": [ "Nexus:1099" ], - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "KoihimeNakamura.CCR", - "UpdateKeys": [ "Nexus:1402" ], - "MapLocalVersions": { - "1.1-20170917": "1.1" - } + "MapLocalVersions": { "1.1-20170917": "1.1" }, + "Default | UpdateKey": "Nexus:1402" }, { // Customizable Traveling Cart Days "ID": "TravelingCartYyeahdude", - "UpdateKeys": [ "Nexus:567" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Mevima.CustomLinens", - "UpdateKeys": [ "Nexus:1027" ], - "MapRemoteVersions": { - "1.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1027" }, { // Custom Shops Redux "ID": "Omegasis.CustomShopReduxGui", - "UpdateKeys": [ "Nexus:1378" ] // added in 1.4.1 + "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 }, { // Custom TV "ID": "Platonymous.CustomTV", - "UpdateKeys": [ "Nexus:1139" ] // added in 1.0.6 + "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 }, { // Daily Luck Message "ID": "Schematix.DailyLuckMessage", - "UpdateKeys": [ "Nexus:1327" ] + "Default | UpdateKey": "Nexus:1327" }, { // Daily News "ID": "bashNinja.DailyNews", - "UpdateKeys": [ "Nexus:1141" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:1141", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Daily Quest Anywhere - "ID": "DailyQuest | Omegasis.DailyQuestAnywhere", // changed in 1.4 - "UpdateKeys": [ "Nexus:513" ] // added in 1.4.1 + "ID": "Omegasis.DailyQuestAnywhere", + "FormerIDs": "DailyQuest", // changed in 1.4 + "Default | UpdateKey": "Nexus:513" // added in 1.4.1 }, { // Debug Mode - "ID": "Pathoschild.Stardew.DebugMode | Pathoschild.DebugMode", // changed in 1.4 - "UpdateKeys": [ "Nexus:679" ] + "ID": "Pathoschild.DebugMode", + "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 + "Default | UpdateKey": "Nexus:679" }, { // Dynamic Checklist "ID": "gunnargolf.DynamicChecklist", - "UpdateKeys": [ "Nexus:1145" ], // added in 1.0.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Bpendragon-DynamicHorses", - "UpdateKeys": [ "Nexus:874" ], - "MapRemoteVersions": { - "1.2": "1.1-release" // manifest not updated - } + "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:874" }, { // Dynamic Machines "ID": "DynamicMachines", - "UpdateKeys": [ "Nexus:374" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.1": "1.1.1" - } + "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 "ID": "BashNinja.DynamicNPCSprites", - "UpdateKeys": [ "Nexus:1183" ] + "Default | UpdateKey": "Nexus:1183" }, { // Easier Farming "ID": "cautiouswafffle.EasierFarming", - "UpdateKeys": [ "Nexus:1426" ] + "Default | UpdateKey": "Nexus:1426" }, { // Empty Hands "ID": "QuicksilverFox.EmptyHands", - "UpdateKeys": [ "Nexus:1176" ], // added in 1.0.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "SPDHealthBar | Speeder.HealthBars", // changed in 1.7.1-pathoschild-update - "UpdateKeys": [ "Nexus:193" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - //"UpdateKeys": [ "Chucklefish:4228" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "ID": "Entoarox.EntoaroxFramework", + "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? + //"Default | UpdateKey": "Chucklefish:4228", // Entoarox opted out of mod update checks + "~1.7.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Expanded Fridge / Dynamic Expanded Fridge "ID": "Uwazouri.ExpandedFridge", - "UpdateKeys": [ "Nexus:1191" ] + "Default | UpdateKey": "Nexus:1191" }, { // Experience Bars - "ID": "ExperienceBars | spacechase0.ExperienceBars", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:509" ] + "ID": "spacechase0.ExperienceBars", + "FormerIDs": "ExperienceBars", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:509" }, { // Extended Bus System "ID": "ExtendedBusSystem", - "UpdateKeys": [ "Chucklefish:4373" ] + "Default | UpdateKey": "Chucklefish:4373" }, { // Extended Fridge - "ID": "Mystra007ExtendedFridge | Crystalmir.ExtendedFridge", // changed in 1.0.1 - "UpdateKeys": [ "Nexus:485" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "ExtendedGreenhouse", - "UpdateKeys": [ "Chucklefish:4303" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 - //"UpdateKeys": [ "Chucklefish:4359" ] // Entoarox opted out of mod update checks + "ID": "Entoarox.ExtendedMinecart", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}" // changed in 1.6.1 + //"Default | UpdateKey": "Chucklefish:4359" // Entoarox opted out of mod update checks }, { // Extended Reach "ID": "spacechase0.ExtendedReach", - "UpdateKeys": [ "Nexus:1493" ] + "Default | UpdateKey": "Nexus:1493" }, { // Fall 28 Snow Day - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:486" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "FarmAutomation.BarnDoorAutomation.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Farm Automation: Item Collector "ID": "FarmAutomation.ItemCollector.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Farm Automation Unofficial: Item Collector "ID": "Maddy99.FarmAutomation.ItemCollector", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Farm Expansion - "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5 | Advize.FarmExpansion", // changed in 2.0, 2.0.5, and 3.0 - "UpdateKeys": [ "Nexus:130" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "FarmResourceGenerator.dll", - "UpdateKeys": [ "Nexus:647" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Pathoschild.FastAnimations", - "UpdateKeys": [ "Nexus:1089" ] + "Default | UpdateKey": "Nexus:1089" }, { // Faster Paths - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths" // changed in 1.2 and 1.3; disambiguate from Shop Expander - // "UpdateKeys": [ "Chucklefish:3641" ] // Entoarox opted out of mod update checks + "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 + // "Default | UpdateKey": "Chucklefish:3641" // Entoarox opted out of mod update checks }, { // Faster Run - "ID": "FasterRun.dll | KathrynHazuka.FasterRun", // changed in 1.1.1-pathoschild-update - "UpdateKeys": [ "Nexus:733" ], // added in 1.1.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "shuaiz.FishingAdjustMod", - "UpdateKeys": [ "Nexus:1350" ] + "Default | UpdateKey": "Nexus:1350" }, { // Fishing Tuner Redux "ID": "HammurabiFishingTunerRedux", - "UpdateKeys": [ "Chucklefish:4578" ] + "Default | UpdateKey": "Chucklefish:4578" }, { // FlorenceMod "ID": "FlorenceMod.dll", - "UpdateKeys": [ "Nexus:591" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.0.1": "1.1" - } + "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 "ID": "spacechase0.FlowerColorPicker", - "UpdateKeys": [ "Nexus:1229" ] + "Default | UpdateKey": "Nexus:1229" }, { // Forage at the Farm "ID": "ForageAtTheFarm", - "UpdateKeys": [ "Nexus:673" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere" // changed in 1.1; disambiguate from Extended Minecart - // "UpdateKeys": [ "Chucklefish:4324" ] // Entoarox opted out of mod update checks + "ID": "Entoarox.FurnitureAnywhere", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}" // changed in 1.1; disambiguate from Extended Minecart + // "Default | UpdateKey": "Chucklefish:4324" // Entoarox opted out of mod update checks }, { // Game Reminder "ID": "mmanlapat.GameReminder", - "UpdateKeys": [ "Nexus:1153" ] + "Default | UpdateKey": "Nexus:1153" }, { // Gate Opener - "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 - "UpdateKeys": [ "GitHub:mralbobo/stardew-gate-opener" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "GenericShopExtender", - "UpdateKeys": [ "Nexus:814" ], // added in 0.1.3 - "Compatibility": { - "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:814", // added in 0.1.3 + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Geode Info Menu "ID": "cat.geodeinfomenu", - "UpdateKeys": [ "Nexus:1448" ] + "Default | UpdateKey": "Nexus:1448" }, { // Get Dressed - "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 - "UpdateKeys": [ "Nexus:331" ], - "Compatibility": { - "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "cat.giantcropring", - "UpdateKeys": [ "Nexus:1182" ] + "Default | UpdateKey": "Nexus:1182" }, { // Gift Taste Helper - "ID": "8008db57-fa67-4730-978e-34b37ef191d6 | tstaples.GiftTasteHelper", // changed in 2.5 - "UpdateKeys": [ "Nexus:229" ], - "Compatibility": { - "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "ShadowDragon.GrandfathersGift", - "UpdateKeys": [ "Nexus:985" ] + "Default | UpdateKey": "Nexus:985" }, { // Happy Animals "ID": "HappyAnimals", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Happy Birthday (Omegasis) - "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'} | Omegasis.HappyBirthday", // changed in 1.4; disambiguate from Oxyligen's fork - "UpdateKeys": [ "Nexus:520" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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) "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "UpdateKeys": [ "Nexus:1064" ] + "Default | UpdateKey": "Nexus:1064" }, { // Harp of Yoba Redux "ID": "Platonymous.HarpOfYobaRedux", - "UpdateKeys": [ "Nexus:914" ] // added in 2.0.3 + "Default | UpdateKey": "Nexus:914" // added in 2.0.3 }, { // Harvest Moon Witch Princess "ID": "Sasara.WitchPrincess", - "UpdateKeys": [ "Nexus:1157" ] + "Default | UpdateKey": "Nexus:1157" }, { // Harvest With Scythe "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "UpdateKeys": [ "Nexus:236" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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) "ID": "icepuente.HorseWhistle", - "UpdateKeys": [ "Nexus:1131" ] + "Default | UpdateKey": "Nexus:1131" }, { // Hunger (Yyeadude) "ID": "HungerYyeadude", - "UpdateKeys": [ "Nexus:613" ] + "Default | UpdateKey": "Nexus:613" }, { // Hunger for Food (Tigerle) "ID": "HungerForFoodByTigerle", - "UpdateKeys": [ "Nexus:810" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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) "ID": "skn.HungerMod", - "UpdateKeys": [ "Nexus:1127" ], - "MapRemoteVersions": { - "1.2.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1127" }, { // Idle Pause "ID": "Veleek.IdlePause", - "UpdateKeys": [ "Nexus:1092" ], - "MapRemoteVersions": { - "1.2": "1.1" // manifest not updated - } + "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1092" }, { // Improved Quality of Life "ID": "Demiacle.ImprovedQualityOfLife", - "UpdateKeys": [ "Nexus:1025" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:1025", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Instant Geode "ID": "InstantGeode", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "~1.12 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Instant Grow Trees - "ID": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 - "UpdateKeys": [ "Nexus:173" ] + "ID": "community.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59", // changed in 1.2 + "Default | UpdateKey": "Nexus:173" }, { // Interaction Helper "ID": "HammurabiInteractionHelper", - "UpdateKeys": [ "Chucklefish:4640" ], // added in 1.0.4-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "cat.autostacker", - "UpdateKeys": [ "Nexus:1184" ], - "MapRemoteVersions": { - "1.0.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1184" }, { // Jiggly Junimo Bundles - "ID": "JJB.dll | Greger.JigglyJunimoBundles", // changed in 1.1.2-pathoschild-update - "UpdateKeys": [ "GitHub:gr3ger/Stardew_JJB" ], // added in 1.0.4-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "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 "ID": "Platonymous.JunimoFarm", - "UpdateKeys": [ "Nexus:984" ], // added in 1.1.3 - "MapRemoteVersions": { - "1.1.2": "1.1.1" // manifest not updated - } + "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:984" // added in 1.1.3 }, { // Less Strict Over-Exertion (AntiExhaustion) "ID": "BALANCEMOD_AntiExhaustion", - "UpdateKeys": [ "Nexus:637" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.0": "1.1" - } + "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 "ID": "Devin Lematty.Level Extender", - "UpdateKeys": [ "Nexus:1471" ], - "MapRemoteVersions": { - "1.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1471" }, { // Level Up Notifications "ID": "Level Up Notifications", - "UpdateKeys": [ "Nexus:855" ] + "Default | UpdateKey": "Nexus:855" }, { // Location and Music Logging "ID": "Brandy Lover.LMlog", - "UpdateKeys": [ "Nexus:1366" ] + "Default | UpdateKey": "Nexus:1366" }, { // Longevity "ID": "RTGOAT.Longevity", - "UpdateKeys": [ "Nexus:649" ] + "Default | UpdateKey": "Nexus:649" }, { // Lookup Anything - "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 - "UpdateKeys": [ "Nexus:541" ], - "Compatibility": { - "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "LoveBubbles", - "UpdateKeys": [ "Nexus:1318" ] + "Default | UpdateKey": "Nexus:1318" }, { // Loved Labels "ID": "LovedLabels.dll", - "UpdateKeys": [ "Nexus:279" ], - "Compatibility": { - "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:279", + "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Luck Skill - "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 - "UpdateKeys": [ "Nexus:521" ], - "Compatibility": { - "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "DIGUS.MailFrameworkMod", - "UpdateKeys": [ "Nexus:1536" ] + "Default | UpdateKey": "Nexus:1536" }, { // MailOrderPigs - "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:632" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 - "UpdateKeys": [ "Nexus:501" ], - "Compatibility": { - "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "MapImageExporter | spacechase0.MapImageExporter", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:1073" ] + "ID": "spacechase0.MapImageExporter", + "FormerIDs": "MapImageExporter", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:1073" }, { // Message Box [API]? (ChatMod) "ID": "Kithio:ChatMod", - "UpdateKeys": [ "Chucklefish:4296" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "MiningAtTheFarm", - "UpdateKeys": [ "Nexus:674" ] + "Default | UpdateKey": "Nexus:674" }, { // Mining With Explosives "ID": "MiningWithExplosives", - "UpdateKeys": [ "Nexus:770" ] + "Default | UpdateKey": "Nexus:770" }, { // Modder Serialization Utility "ID": "SerializerUtils-0-1", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." }, { // More Artifact Spots "ID": "451", - "UpdateKeys": [ "Nexus:451" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Platonymous.MoreMapLayers", - "UpdateKeys": [ "Nexus:1134" ] // added in 1.1.1 + "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 }, { // More Pets - "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - // "UpdateKeys": [ "Chucklefish:4288" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility - } + "ID": "Entoarox.MorePets", + "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS", // changed in 1.3 + // "Default | UpdateKey": "Chucklefish:4288", // Entoarox opted out of mod update checks + "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility }, { // More Rain - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:441" ], // added in 1.5.1 - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Joco80.MoreWeapons", - "UpdateKeys": [ "Nexus:1168" ] + "Default | UpdateKey": "Nexus:1168" }, { // Move Faster "ID": "shuaiz.MoveFasterMod", - "UpdateKeys": [ "Nexus:1351" ] + "Default | UpdateKey": "Nexus:1351" }, { // Multiple Sprites and Portraits On Rotation (File Loading) "ID": "FileLoading", - "UpdateKeys": [ "Nexus:1094" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.1": "1.12" - } + "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 - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'} | Omegasis.MuseumRearranger", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:428" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "UpdateKeys": [ "Chucklefish:3683" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere - "UpdateKeys": [ "Nexus:433" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "2.1": "1.3" // 1.3 had wrong version in manifest - } + "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 "ID": "Hangy.NoKidsEver", - "UpdateKeys": [ "Nexus:1464" ] + "Default | UpdateKey": "Nexus:1464" }, { // No Debug Mode "ID": "NoDebugMode", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "debug mode was removed in SMAPI 1.0." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." }, { // No Fence Decay "ID": "cat.nofencedecay", - "UpdateKeys": [ "Nexus:1180" ] + "Default | UpdateKey": "Nexus:1180" }, { // No More Pets - "ID": "NoMorePets | Omegasis.NoMorePets", // changed in 1.4 - "UpdateKeys": [ "Nexus:506" ] // added in 1.4.1 + "ID": "Omegasis.NoMorePets", + "FormerIDs": "NoMorePets", // changed in 1.4 + "Default | UpdateKey": "Nexus:506" // added in 1.4.1 }, { // NoSoilDecay "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "UpdateKeys": [ "Nexus:237" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location - } + "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 "ID": "Platonymous.NoSoilDecayRedux", - "UpdateKeys": [ "Nexus:1084" ] // added in 1.1.9 + "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 }, { // NPC Map Locations "ID": "NPCMapLocationsMod", - "UpdateKeys": [ "Nexus:239" ], - "Compatibility": { - "1.42~1.43": { - "Status": "AssumeBroken", - "ReasonPhrase": "this version has an update check error which crashes the game." - } - } + "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 "ID": "NpcEcho.dll", - "UpdateKeys": [ "Nexus:694" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "spacechase0.ObjectTimeLeft", - "UpdateKeys": [ "Nexus:1315" ] + "Default | UpdateKey": "Nexus:1315" }, { // OmniFarm - "ID": "BlueMod_OmniFarm | PhthaloBlue.OmniFarm", // changed in 2.0.2-pathoschild-update - "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_OmniFarm" ], - "Compatibility": { - "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "midoriarmstrong.seasonalitems", - "UpdateKeys": [ "Nexus:1452" ] + "Default | UpdateKey": "Nexus:1452" }, { // Part of the Community "ID": "SB_PotC", - "UpdateKeys": [ "Nexus:923" ], - "Compatibility": { - "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "Default | UpdateKey": "Nexus:923", + "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 }, { // PelicanFiber - "ID": "PelicanFiber.dll | jwdred.PelicanFiber", // changed in 3.0.1 - "UpdateKeys": [ "Nexus:631" ], - "Compatibility": { - "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "3.0.2": "3.0.1" // didn't change manifest version - } + "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 "ID": "Platonymous.PelicanTTS", - "UpdateKeys": [ "Nexus:1079" ], // added in 1.6.1 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "UpdateKeys": [ "Nexus:1419" ] + "Default | UpdateKey": "Nexus:1419" }, { // Persival's BundleMod "ID": "BundleMod.dll", - "UpdateKeys": [ "Nexus:438" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } + "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 "ID": "Demiacle.PlantOnGrass", - "UpdateKeys": [ "Nexus:1026" ] + "Default | UpdateKey": "Nexus:1026" }, { // Point-and-Plant - "ID": "PointAndPlant.dll | jwdred.PointAndPlant", // changed in 1.0.3 - "UpdateKeys": [ "Nexus:572" ], - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "BadNetCode.PonyWeightLossProgram", - "UpdateKeys": [ "Nexus:1232" ] + "Default | UpdateKey": "Nexus:1232" }, { // Portraiture "ID": "Platonymous.Portraiture", - "UpdateKeys": [ "Nexus:999" ] // added in 1.3.1 + "Default | UpdateKey": "Nexus:999" // added in 1.3.1 }, { // Prairie King Made Easy - "ID": "PrairieKingMadeEasy.dll | Mucchan.PrairieKingMadeEasy", // changed in 1.0.1 - "UpdateKeys": [ "Chucklefish:3594" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "BadNetCode.QuestDelay", - "UpdateKeys": [ "Nexus:1239" ] + "Default | UpdateKey": "Nexus:1239" }, { // Rain Randomizer "ID": "RainRandomizer.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Recatch Legendary Fish - "ID": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 - "UpdateKeys": [ "Nexus:172" ] + "ID": "community.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9", // changed in 1.3 + "Default | UpdateKey": "Nexus:172" }, { // Regeneration "ID": "HammurabiRegeneration", - "UpdateKeys": [ "Chucklefish:4584" ] + "Default | UpdateKey": "Chucklefish:4584" }, { // Relationship Bar UI "ID": "RelationshipBar", - "UpdateKeys": [ "Nexus:1009" ] + "Default | UpdateKey": "Nexus:1009" }, { // RelationshipsEnhanced "ID": "relationshipsenhanced", - "UpdateKeys": [ "Chucklefish:4435" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "relationshipstatus", - "UpdateKeys": [ "Nexus:751" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.0.5": "1.0.4" // not updated in manifest - } + "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 "ID": "JarvieK.RentedTools", - "UpdateKeys": [ "Nexus:1307" ] + "Default | UpdateKey": "Nexus:1307" }, { // Replanter - "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 - "UpdateKeys": [ "Nexus:589" ], - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "lrsk_sdvm_rerg.0925160827", - "UpdateKeys": [ "Chucklefish:4465" ], - "MapLocalVersions": { - "1.1.2-release": "1.1.2" - } + "MapLocalVersions": { "1.1.2-release": "1.1.2" }, + "Default | UpdateKey": "Chucklefish:4465" }, { // Reseed "ID": "Roc.Reseed", - "UpdateKeys": [ "Nexus:887" ] + "Default | UpdateKey": "Nexus:887" }, { // Reusable Wallpapers and Floors (Wallpaper Retain) "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "UpdateKeys": [ "Nexus:356" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Platonymous.RingOfFire", - "UpdateKeys": [ "Nexus:1166" ] // added in 1.0.1 + "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 }, { // Rope Bridge "ID": "RopeBridge", - "UpdateKeys": [ "Nexus:824" ] + "Default | UpdateKey": "Nexus:824" }, { // Rotate Toolbar "ID": "Pathoschild.RotateToolbar", - "UpdateKeys": [ "Nexus:1100" ] + "Default | UpdateKey": "Nexus:1100" }, { // Rush Orders - "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 - "UpdateKeys": [ "Nexus:605" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "ID": "spacechase0.RushOrders", + "FormerIDs": "RushOrders", // changed in 1.1 + "Default | UpdateKey": "Nexus:605", + "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, { // Save Anywhere - "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl - "UpdateKeys": [ "Nexus:444" ], // added in 2.6.1 - "Compatibility": { - "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'} | Omegasis.SaveBackup", // changed in 1.3; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:435" ], // added in 1.3.1 - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "caraxian.scroll.to.blank", - "UpdateKeys": [ "Chucklefish:4405" ] + "Default | UpdateKey": "Chucklefish:4405" }, { // Scythe Harvesting - "ID": "ScytheHarvesting | mmanlapat.ScytheHarvesting", // changed in 1.6 - "UpdateKeys": [ "Nexus:1106" ] + "ID": "mmanlapat.ScytheHarvesting", + "FormerIDs": "ScytheHarvesting", // changed in 1.6 + "Default | UpdateKey": "Nexus:1106" }, { // Seasonal Immersion - "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - // "UpdateKeys": [ "Chucklefish:4262" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "ID": "Entoarox.SeasonalImmersion", + "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + // "Default | UpdateKey": "Chucklefish:4262", // Entoarox opted out of mod update checks + "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Seed Bag "ID": "Platonymous.SeedBag", - "UpdateKeys": [ "Nexus:1133" ] // added in 1.1.2 + "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 }, { // Self Service "ID": "JarvieK.SelfService", - "UpdateKeys": [ "Nexus:1304" ], - "MapRemoteVersions": { - "0.2.1": "0.2" // manifest not updated - } + "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated + "Default | UpdateKey": "Nexus:1304" }, { // Send Items "ID": "Denifia.SendItems", - "UpdateKeys": [ "Nexus:1087" ], // added in 1.0.3 (2017-10-04) - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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) "ID": "TheCroak.BuildingsNotifications", - "UpdateKeys": [ "Nexus:620" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Shenandoah Project", - "UpdateKeys": [ "Nexus:756" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.1.1": "1.1" // not updated in manifest - } + "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 "ID": "spacechase0.ShipAnywhere", - "UpdateKeys": [ "Nexus:1379" ] + "Default | UpdateKey": "Nexus:1379" }, { // Shipment Tracker "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "UpdateKeys": [ "Nexus:321" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - // "UpdateKeys": [ "Chucklefish:4381" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 + // "Default | UpdateKey": "Chucklefish:4381", // Entoarox opted out of mod update checks + "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Showcase Mod "ID": "Igorious.Showcase", - "UpdateKeys": [ "Chucklefish:4487" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.9-500": "0.9" - } + "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 "ID": "TehPers.ShroomSpotter", - "UpdateKeys": [ "Nexus:908" ] + "Default | UpdateKey": "Nexus:908" }, { // Simple Crop Label "ID": "SimpleCropLabel", - "UpdateKeys": [ "Nexus:314" ] + "Default | UpdateKey": "Nexus:314" }, { // Simple Sound Manager "ID": "Omegasis.SimpleSoundManager", - "UpdateKeys": [ "Nexus:1410" ], // added in 1.0.1 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", // can remove once 1.0.1 is published - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" // can remove once 1.0.1 is published }, { // Simple Sprinklers - "ID": "SimpleSprinkler.dll | tZed.SimpleSprinkler", // changed in 1.5 - "UpdateKeys": [ "Nexus:76" ], - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "6266959802", - "UpdateKeys": [ "Nexus:366" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) - }, - "MapLocalVersions": { - "0.0": "1.4" - } + "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 - "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b | alphablackwolf.skillPrestige", // changed circa 1.2.3 - "UpdateKeys": [ "Nexus:569" ], - "Compatibility": { - "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63 | Alphablackwolf.CookingSkillPrestigeAdapter", // changed circa 1.1 - "UpdateKeys": [ "Nexus:569" ], - "Compatibility": { - "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.2.3": "1.1" // manifest not updated - } + "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 - "ID": "SkipIntro | Pathoschild.SkipIntro", // changed in 1.4 - "UpdateKeys": [ "Nexus:533" ] + "ID": "Pathoschild.SkipIntro", + "FormerIDs": "SkipIntro", // changed in 1.4 + "Default | UpdateKey": "Nexus:533" }, { // Skull Cavern Elevator "ID": "SkullCavernElevator", - "UpdateKeys": [ "Nexus:963" ] + "Default | UpdateKey": "Nexus:963" }, { // Skull Cave Saver - "ID": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 - "UpdateKeys": [ "Nexus:175" ] + "ID": "community.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774", // changed in 1.1 + "Default | UpdateKey": "Nexus:175" }, { // Sleepy Eye "ID": "spacechase0.SleepyEye", - "UpdateKeys": [ "Nexus:1152" ] + "Default | UpdateKey": "Nexus:1152" }, { // Slower Fence Decay - "ID": "SPDSlowFenceDecay | Speeder.SlowerFenceDecay", // changed in 0.5.2-pathoschild-update - "UpdateKeys": [ "Nexus:252" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "KuroBear.SmartMod", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~2.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Solar Eclipse Event "ID": "KoihimeNakamura.SolarEclipseEvent", - "UpdateKeys": [ "Nexus:897" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.3-20170917": "1.3" - } + "Default | UpdateKey": "Nexus:897", + "MapLocalVersions": { "1.3-20170917": "1.3" }, + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // SpaceCore "ID": "spacechase0.SpaceCore", - "UpdateKeys": [ "Nexus:1348" ] + "Default | UpdateKey": "Nexus:1348" }, { // Speedster "ID": "Platonymous.Speedster", - "UpdateKeys": [ "Nexus:1102" ] // added in 1.3.1 + "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 }, { // Sprinkler Range "ID": "cat.sprinklerrange", - "UpdateKeys": [ "Nexus:1179" ], - "MapRemoteVersions": { - "1.0.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1179" }, { // Sprinkles "ID": "Platonymous.Sprinkles", - "UpdateKeys": [ "Chucklefish:4592" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "SPDSprintAndDash", - "UpdateKeys": [ "Chucklefish:3531" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "lrsk_sdvm_sndr.0921161059 | littleraskol.SprintAndDashRedux", // changed in 1.3 - "UpdateKeys": [ "Chucklefish:4201" ] + "ID": "littleraskol.SprintAndDashRedux", + "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 + "Default | UpdateKey": "Chucklefish:4201" }, { // Sprinting Mod "ID": "a10d3097-b073-4185-98ba-76b586cba00c", - "UpdateKeys": [ "GitHub:oliverpl/SprintingMod" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapLocalVersions": { - "1.0": "2.1" // not updated in manifest - } + "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 - "ID": "StackSplitX.dll | tstaples.StackSplitX", // changed circa 1.3.1 - "UpdateKeys": [ "Nexus:798" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "StaminaRegen.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Stardew Config Menu "ID": "Juice805.StardewConfigMenu", - "UpdateKeys": [ "Nexus:1312" ] + "Default | UpdateKey": "Nexus:1312" }, { // Stardew Content Compatibility Layer (SCCL) "ID": "SCCL", - "UpdateKeys": [ "Nexus:889" ], - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "Default | UpdateKey": "Nexus:889", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, { // Stardew Editor Game Integration "ID": "spacechase0.StardewEditor.GameIntegration", - "UpdateKeys": [ "Nexus:1298" ] + "Default | UpdateKey": "Nexus:1298" }, { // Stardew Notification "ID": "stardewnotification", - "UpdateKeys": [ "GitHub:monopandora/StardewNotification" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'} | Omegasis.StardewSymphony", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:425" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "StarDustCore", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." }, { // Starting Money - "ID": "StartingMoney | mmanlapat.StartingMoney", // changed in 1.1 - "UpdateKeys": [ "Nexus:1138" ] + "ID": "mmanlapat.StartingMoney", + "FormerIDs": "StartingMoney", // changed in 1.1 + "Default | UpdateKey": "Nexus:1138" }, { // StashItemsToChest "ID": "BlueMod_StashItemsToChest", - "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_StashItemsToChest" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "stephansstardewcrops", - "UpdateKeys": [ "Chucklefish:4314" ], - "MapRemoteVersions": { - "1.41": "1.1" // manifest not updated - } + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "Default | UpdateKey": "Chucklefish:4314" }, { // Stone Bridge Over Pond (PondWithBridge) "ID": "PondWithBridge.dll", - "UpdateKeys": [ "Nexus:316" ], - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.0": "1.0" - } + "MapLocalVersions": { "0.0": "1.0" }, + "Default | UpdateKey": "Nexus:316", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Stumps to Hardwood Stumps "ID": "StumpsToHardwoodStumps", - "UpdateKeys": [ "Nexus:691" ] + "Default | UpdateKey": "Nexus:691" }, { // Super Greenhouse Warp Modifier "ID": "SuperGreenhouse", - "UpdateKeys": [ "Chucklefish:4334" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "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 "ID": "Platonymous.SwimSuit", - "UpdateKeys": [ "Nexus:1215" ] // added in 0.5.1 + "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 }, { // Tainted Cellar "ID": "TaintedCellar.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 - } + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Tapper Ready "ID": "skunkkk.TapperReady", - "UpdateKeys": [ "Nexus:1219" ] + "Default | UpdateKey": "Nexus:1219" }, { // Teh's Fishing Overhaul "ID": "TehPers.FishingOverhaul", - "UpdateKeys": [ "Nexus:866" ] + "Default | UpdateKey": "Nexus:866" }, { // Teleporter "ID": "Teleporter", - "UpdateKeys": [ "Chucklefish:4374" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 "ID": "Pathoschild.TheLongNight", - "UpdateKeys": [ "Nexus:1369" ] + "Default | UpdateKey": "Nexus:1369" }, { // Three-heart Dance Partner "ID": "ThreeHeartDancePartner", - "UpdateKeys": [ "Nexus:500" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "Default | UpdateKey": "Nexus:500", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, { // TimeFreeze - "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe | Omegasis.TimeFreeze", // changed in 1.2 - "UpdateKeys": [ "Nexus:973" ] // added in 1.2.1 + "ID": "Omegasis.TimeFreeze", + "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 + "Default | UpdateKey": "Nexus:973" // added in 1.2.1 }, { // Time Reminder "ID": "KoihimeNakamura.TimeReminder", - "UpdateKeys": [ "Nexus:1000" ], - "MapLocalVersions": { - "1.0-20170314": "1.0.2" - } + "MapLocalVersions": { "1.0-20170314": "1.0.2" }, + "Default | UpdateKey": "Nexus:1000" }, { // TimeSpeed - "ID": "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 and 2.1; disambiguate other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:169" ], - "Compatibility": { - "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "ID": "community.TimeSpeed", + "FormerIDs": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'}", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:169", + "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, { // TractorMod - "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "UpdateKeys": [ "Nexus:1401" ] + "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 "ID": "SMAPI.TrainerMod", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." }, { // Tree Transplant "ID": "TreeTransplant", - "UpdateKeys": [ "Nexus:1342" ] + "Default | UpdateKey": "Nexus:1342" }, { // UI Info Suite "ID": "Cdaragorn.UiInfoSuite", - "UpdateKeys": [ "Nexus:1150" ] + "Default | UpdateKey": "Nexus:1150" }, { // UiModSuite "ID": "Demiacle.UiModSuite", - "UpdateKeys": [ "Nexus:1023" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapLocalVersions": { - "0.5": "1.0" // not updated in manifest - } + "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 "ID": "dantheman999.VariableGrass", - "UpdateKeys": [ "GitHub:dantheman999301/StardewMods" ] + "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" }, { // Vertical Toolbar "ID": "SB_VerticalToolMenu", - "UpdateKeys": [ "Nexus:943" ] + "Default | UpdateKey": "Nexus:943" }, { // WakeUp "ID": "WakeUp.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // Wallpaper Fix "ID": "WallpaperFix.dll", - "UpdateKeys": [ "Chucklefish:4211" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Chucklefish:4211", + "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // WarpAnimals "ID": "Symen.WarpAnimals", - "UpdateKeys": [ "Nexus:1400" ] + "Default | UpdateKey": "Nexus:1400" }, { // Weather Controller "ID": "WeatherController.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "~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 "ID": "WhatAMush", - "UpdateKeys": [ "Nexus:1097" ] + "Default | UpdateKey": "Nexus:1097" }, { // WHats Up "ID": "wHatsUp", - "UpdateKeys": [ "Nexus:1082" ] + "Default | UpdateKey": "Nexus:1082" }, { // Wonderful Farm Life "ID": "WonderfulFarmLife.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 - } + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, { // XmlSerializerRetool "ID": "XmlSerializerRetool.dll", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." - } - } + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." }, { // Xnb Loader "ID": "Entoarox.XnbLoader", - // "UpdateKeys": [ "Chucklefish:4506" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + // "Default | UpdateKey": "Chucklefish:4506", // Entoarox opted out of mod update checks + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // zDailyIncrease "ID": "zdailyincrease", - "UpdateKeys": [ "Chucklefish:4247" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapRemoteVersions": { - "1.3.5": "1.3.4" // not updated in manifest - } + "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 - "ID": "ZoomMod | RockinMods.ZoomMod", // changed circa 1.2.1 - "UpdateKeys": [ "Nexus:1326" ], - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "56d3439c-7b9b-497e-9496-0c4890e8a00e | Zoryn.DurableFences", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] + "ID": "Zoryn.DurableFences", + "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" }, { // Zoryn's Health Bars - "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "fa277b1f-265e-47c3-a84f-cd320cc74949 | Zoryn.FishingMod", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] + "ID": "Zoryn.FishingMod", + "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" }, { // Zoryn's Junimo Deposit Anywhere - "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 - "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "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 cd6c4ac3..129c88b0 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -89,7 +89,9 @@ - + + + @@ -110,9 +112,7 @@ - - @@ -154,7 +154,6 @@ - -- 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/ModLoading') 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 9b3dd42cbf62a8524ac390d9418cf961c502868a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 23:02:04 -0500 Subject: encapsulate update key to URL logic for reuse (#437) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 14 ++++++------ src/SMAPI/Constants.cs | 33 ++++++++++++++++++++------- src/SMAPI/Framework/ModLoading/ModResolver.cs | 16 ++++--------- src/SMAPI/Program.cs | 2 +- 4 files changed, 38 insertions(+), 27 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 900a6c4f..d63eb1a2 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Tests.Core [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -131,7 +131,7 @@ namespace StardewModdingAPI.Tests.Core mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); @@ -149,7 +149,7 @@ namespace StardewModdingAPI.Tests.Core }); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -164,7 +164,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -178,7 +178,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -195,7 +195,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mod); // act - new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); @@ -221,7 +221,7 @@ namespace StardewModdingAPI.Tests.Core mock.Setup(p => p.DirectoryPath).Returns(modFolder); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 515e9870..caee93e4 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -21,6 +21,14 @@ namespace StardewModdingAPI /// Whether the directory containing the current save's data exists on disk. private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. + private static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", + ["GitHub"] = "https://github.com/{0}/releases" + }; + /********* ** Accessors @@ -87,14 +95,6 @@ namespace StardewModdingAPI Platform.Mono; #endif - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API. - internal static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", - ["GitHub"] = "https://github.com/{0}/releases" - }; - /********* ** Internal methods @@ -145,6 +145,23 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } + /// Get an update URL for an update key (if valid). + /// The update key. + internal static string GetUpdateUrl(string updateKey) + { + string[] parts = updateKey.Split(new[] { ':' }, 2); + if (parts.Length != 2) + return null; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (Constants.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + return string.Format(urlTemplate, modID); + + return null; + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 09a9299e..99d86bf8 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -73,8 +73,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// Validate manifest metadata. /// The mod manifests to validate. /// The current SMAPI version. - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). - public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, IDictionary vendorModUrls) + /// Get an update URL for an update key (if valid). + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, Func getUpdateUrl) { mods = mods.ToArray(); @@ -101,15 +101,9 @@ namespace StardewModdingAPI.Framework.ModLoading List updateUrls = new List(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { - string[] parts = key.Split(new[] { ':' }, 2); - if (parts.Length != 2) - continue; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - updateUrls.Add(string.Format(urlTemplate, modID)); + string url = getUpdateUrl(key); + if (url != null) + updateUrls.Add(url); } if (mod.DataRecord.AlternativeUrl != null) updateUrls.Add(mod.DataRecord.AlternativeUrl); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index ec841f4c..85bc83a7 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -360,7 +360,7 @@ namespace StardewModdingAPI // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), modDatabase).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); + resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); -- cgit From 8a1982326799dfe7f3d078beba32348befa1c9e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 23:12:01 -0500 Subject: add mod page URL to missing-dependency errors (#437) --- docs/release-notes.md | 4 +- src/SMAPI/Framework/ModData/ModDatabase.cs | 58 ++++++++++++++++++++++----- src/SMAPI/Framework/ModLoading/ModResolver.cs | 5 ++- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.config.json | 12 ++++-- 5 files changed, 63 insertions(+), 18 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6b0c5d28..831d89b0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,9 +1,9 @@ # Release notes ## 2.5 * For players: - * Dependency errors will now show the name of the missing mod, instead of its ID. + * When a mod is skipped because you're missing a dependency, the error now shows the name and URL of the missing mod. * Fixed mod crashes being logged under `[SMAPI]` instead of the mod name. - * Updated compatibility list and enabled update checks for more older mods. + * Updated compatibility list and enabled update checks for more old mods. * For modders: * Fixed error when accessing a mod-provided API whose underlying class is `internal`. diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs index af067f8f..332c5c48 100644 --- a/src/SMAPI/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI/Framework/ModData/ModDatabase.cs @@ -15,19 +15,24 @@ namespace StardewModdingAPI.Framework.ModData /// The underlying mod data records indexed by default display name. private readonly IDictionary Records; + /// Get an update URL for an update key (if valid). + private readonly Func GetUpdateUrl; + /********* ** Public methods *********/ /// Construct an empty instance. public ModDatabase() - : this(new Dictionary()) { } + : this(new Dictionary(), key => null) { } /// Construct an instance. /// The underlying mod data records indexed by default display name. - public ModDatabase(IDictionary records) + /// Get an update URL for an update key (if valid). + public ModDatabase(IDictionary records, Func getUpdateUrl) { this.Records = records; + this.GetUpdateUrl = getUpdateUrl; } /// Get a parsed representation of the which match a given manifest. @@ -74,21 +79,54 @@ namespace StardewModdingAPI.Framework.ModData /// 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 this.TryGetRaw(id, out string displayName, out ModDataRecord _) + ? displayName + : null; + } - return null; + /// Get the mod page URL for a mod (if available). + /// The unique mod ID. + public string GetModPageUrlFor(string id) + { + // get raw record + if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) + return null; + + // get update key + ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + if (updateKeyField == null) + return null; + + // get update URL + return this.GetUpdateUrl(updateKeyField.Value); } /********* ** Private models *********/ - /// Get the data record matching a given manifest. - /// The mod manifest. + /// Get a raw data record. + /// The mod ID to match. + /// The mod's default display name. + /// The raw mod record. + private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) + { + foreach (var entry in this.Records) + { + if (entry.Value.ID != null && entry.Value.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + { + displayName = entry.Key; + record = entry.Value; + return true; + } + } + + displayName = null; + record = null; + return false; + } + /// Get a raw data record. + /// The mod manifest whose fields to match. /// The mod's default display name. /// The raw mod record. private bool TryGetRaw(IManifest manifest, out string displayName, out ModDataRecord record) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 99d86bf8..b46ee117 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -272,8 +272,11 @@ namespace StardewModdingAPI.Framework.ModLoading from entry in dependencies where entry.IsRequired && entry.Mod == null let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + let modUrl = modDatabase.GetModPageUrlFor(entry.ID) orderby displayName - select displayName + select modUrl != null + ? $"{displayName}: {modUrl}" + : displayName ).ToArray(); if (failedModNames.Any()) { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 85bc83a7..275876dd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -351,7 +351,7 @@ namespace StardewModdingAPI 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); + ModDatabase modDatabase = new ModDatabase(this.Settings.ModData, Constants.GetUpdateUrl); // load mods { diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index ff5b1f9d..a9d3673c 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -458,7 +458,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Custom Crops": { - "ID": "spacechase0.CustomCrops" + "ID": "spacechase0.CustomCrops", + "Default | UpdateKey": "Nexus:1592" }, "Custom Element Handler": { @@ -515,7 +516,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Custom NPC": { - "ID": "Platonymous.CustomNPC" + "ID": "Platonymous.CustomNPC", + "Default | UpdateKey": "Nexus:1607" }, "Custom Shops Redux": { @@ -890,7 +892,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Json Assets": { - "ID": "spacechase0.JsonAssets" + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720" }, "Junimo Farm": { @@ -1172,7 +1175,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "PyTK - Platonymous Toolkit": { - "ID": "Platonymous.Toolkit" + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" }, "Point-and-Plant": { -- cgit From e64326f9fe5a388e3a0638567bf4bdf8aab4b639 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 16:38:45 -0500 Subject: Revert "rewrite all mod assemblies to let SMAPI proxy into their internal classes (#435)" This reverts commit 032997650010a9b6cd3378cb1a2b8273fb3f56ff. --- docs/release-notes.md | 2 - src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 80 +++++++++++++----------- 2 files changed, 42 insertions(+), 40 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index f44db00c..228fec82 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,11 +8,9 @@ * Updated compatibility list and enabled update checks for more old mods. * For modders: - * Fixed error when accessing a mod-provided API whose underlying class is `internal`. * Fixed deadlock in rare cases when injecting a file with an asset loader. * For SMAPI developers: - * All mod assemblies are now rewritten in-memory to support features like mod-provided APIs. * Overhauled `StardewModdingApi.config.json`'s `ModData` format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. ## 2.4 diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index ac849971..3a7b214a 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; @@ -95,14 +94,23 @@ namespace StardewModdingAPI.Framework.ModLoading if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + if (changed) { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + { + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } } @@ -184,48 +192,38 @@ namespace StardewModdingAPI.Framework.ModLoading /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private void RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; - // let SMAPI proxy mod internals for mod-provided APIs - { - MethodReference attributeConstructor = module.Import(typeof(InternalsVisibleToAttribute).GetConstructor(new[] { typeof(string) })); - CustomAttribute attribute = new CustomAttribute(attributeConstructor); - attribute.ConstructorArguments.Add(new CustomAttributeArgument(module.TypeSystem.String, "StardewModdingAPI.Proxies")); - assembly.CustomAttributes.Add(attribute); - } - // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) { - for (int i = 0; i < module.AssemblyReferences.Count; i++) + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) - { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; - } - } - - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; } } + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); + } // find (and optionally rewrite) incompatible instructions + bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { @@ -234,6 +232,8 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; } // check CIL instructions @@ -244,9 +244,13 @@ namespace StardewModdingAPI.Framework.ModLoading { InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; } } } + + return platformChanged || anyRewritten; } /// Process the result from an instruction handler. -- 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/ModLoading') 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 258e4c16e3d58256304854f9cd9633f0ff480375 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 13:56:54 -0500 Subject: fix default update keys not being applied (#439) --- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 3 --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index 5a6561a7..7f49790d 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.ModData /// 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; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index be73254d..8b4a3eb8 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -57,6 +57,13 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + // apply defaults + if (manifest != null && dataRecord != null) + { + if (dataRecord.UpdateKey != null) + manifest.UpdateKeys = new[] { dataRecord.UpdateKey }; + } + // build metadata ModMetadataStatus status = error == null ? ModMetadataStatus.Found -- cgit From 674618664a72679812c1b51065f725fec99aa86d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 19:32:27 -0500 Subject: add unvalidated update tick event for specialised use cases (#446) --- docs/release-notes.md | 3 ++- src/SMAPI/Events/SpecialisedEvents.cs | 26 ++++++++++++++++++++++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 5 +++++ .../ModLoading/InstructionHandleResult.cs | 7 +++++- src/SMAPI/Framework/SGame.cs | 6 +++-- src/SMAPI/Metadata/InstructionMetadata.cs | 1 + src/SMAPI/StardewModdingAPI.csproj | 3 ++- 7 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI/Events/SpecialisedEvents.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index f2dde81d..597fe0e5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,8 +9,9 @@ * Updated compatibility list and enabled update checks for more mods. * For modders: - * Added APIs to fetch and interact with content packs. + * Added content pack APIs. * Added support for `ISemanticVersion` in JSON models. + * Added `SpecialisedEvents.UnvalidatedUpdateTick` event for specialised use cases. * Fixed deadlock in rare cases when injecting a file with an asset loader. * Fixed unhelpful error when a mod exposes a non-public API. * Fixed input events being raised for keyboard buttons when a textbox is receiving input. diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs new file mode 100644 index 00000000..2a36e6e4 --- /dev/null +++ b/src/SMAPI/Events/SpecialisedEvents.cs @@ -0,0 +1,26 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mod. + public static class SpecialisedEvents + { + /********* + ** Events + *********/ + /// Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console. + public static event EventHandler UnvalidatedUpdateTick; + + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeUnvalidatedUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SpecialisedEvents)}.{nameof(SpecialisedEvents.UnvalidatedUpdateTick)}", SpecialisedEvents.UnvalidatedUpdateTick?.GetInvocationList()); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 3a7b214a..ccbd053e 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,6 +285,11 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; + case InstructionHandleResult.DetectedUnvalidatedUpdateTick: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); + break; + case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index 0ae598fc..6a7e0519 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Events; + namespace StardewModdingAPI.Framework.ModLoading { /// Indicates how an instruction was handled. @@ -19,6 +21,9 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedSaveSerialiser, /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. - DetectedDynamic + DetectedDynamic, + + /// The instruction is compatible, but references which may impact stability. + DetectedUnvalidatedUpdateTick } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index f080f3c5..73c87118 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -229,6 +229,7 @@ namespace StardewModdingAPI.Framework if (SGame._newDayTask != null) { base.Update(gameTime); + SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); return; } @@ -236,6 +237,7 @@ namespace StardewModdingAPI.Framework if (Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); + SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); return; } @@ -267,6 +269,7 @@ namespace StardewModdingAPI.Framework // suppress non-save events base.Update(gameTime); + SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); return; } if (this.IsBetweenCreateEvents) @@ -289,9 +292,7 @@ namespace StardewModdingAPI.Framework ** Game loaded events *********/ if (this.FirstUpdate) - { GameEvents.InvokeInitialize(this.Monitor); - } /********* ** Locale changed events @@ -556,6 +557,7 @@ namespace StardewModdingAPI.Framework /********* ** Update events *********/ + SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); if (this.FirstUpdate) { this.FirstUpdate = false; diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index f285764c..5bb461c1 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -74,6 +74,7 @@ namespace StardewModdingAPI.Metadata new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), + new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick), /**** ** rewrite CIL to fix incompatible code diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 7cf62a91..562da60d 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -284,4 +285,4 @@ - + \ No newline at end of file -- cgit From 9369232118d0ae08df49bbc30037387cf71dd861 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 20:29:52 -0500 Subject: replace manual relative path logic with new path utilities --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 3 ++- src/SMAPI/Program.cs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 8b4a3eb8..ba6dab1a 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -6,6 +6,7 @@ using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.ModLoading { @@ -55,7 +56,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); // apply defaults if (manifest != null && dataRecord != null) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 825a8401..2d0908a1 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -25,6 +25,7 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; @@ -660,7 +661,7 @@ namespace StardewModdingAPI { // 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); + this.Monitor.Log($"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)} (content pack)...", LogLevel.Trace); // validate status if (metadata.Status == ModMetadataStatus.Failed) @@ -702,7 +703,7 @@ namespace StardewModdingAPI // get basic info IManifest manifest = metadata.Manifest; this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $"Loading {metadata.DisplayName} from {metadata.DirectoryPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid + ? $"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid : $"Loading {metadata.DisplayName}...", LogLevel.Trace); // validate status -- cgit From d7696912e007a2b455a2fd5e1974924d2efe83b3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Feb 2018 16:51:37 -0500 Subject: reimplement log parser with serverside parsing and vue.js frontend --- docs/release-notes.md | 6 + src/SMAPI.Web/Controllers/LogParserController.cs | 35 ++- .../Framework/LogParsing/LogParseException.cs | 15 + src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 222 ++++++++++++++ .../Framework/LogParsing/Models/LogLevel.cs | 24 ++ .../Framework/LogParsing/Models/LogMessage.cs | 24 ++ .../Framework/LogParsing/Models/ModInfo.cs | 24 ++ .../Framework/LogParsing/Models/ParsedLog.cs | 47 +++ src/SMAPI.Web/ViewModels/LogParserModel.cs | 9 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 219 ++++++++------ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 325 ++++++++++----------- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 321 ++++++-------------- src/SMAPI.sln | 2 +- .../ModLoading/InstructionHandleResult.cs | 2 +- 14 files changed, 771 insertions(+), 504 deletions(-) create mode 100644 src/SMAPI.Web/Framework/LogParsing/LogParseException.cs create mode 100644 src/SMAPI.Web/Framework/LogParsing/LogParser.cs create mode 100644 src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs create mode 100644 src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs create mode 100644 src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs create mode 100644 src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 08f945e4..03b6dd77 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,8 +20,14 @@ * Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity. * Fixed some JSON field names being case-sensitive. +* For the [log parser][]: + * Significantly reduced download size when viewing files with repeated errors. + * Improved parse error handling. + * Fixed 'log started' field showing incorrect date. + * For SMAPI developers: * Overhauled mod DB format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. + * Reimplemented log parser with serverside parsing and vue.js on the frontend. ## 2.4 * For players: diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 04a11a82..62547deb 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.LogParsing; +using StardewModdingAPI.Web.Framework.LogParsing.Models; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers @@ -52,25 +54,23 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] [Route("log")] [Route("log/{id}")] - public ViewResult Index(string id = null) + public async Task Index(string id = null) { - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id)); + // fresh page + if (string.IsNullOrWhiteSpace(id)) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null)); + + // log page + PasteInfo paste = await this.GetAsync(id); + ParsedLog log = paste.Success + ? new LogParser().Parse(paste.Content) + : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error }; + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log)); } /*** ** JSON ***/ - /// Fetch raw text from Pastebin. - /// The Pastebin paste ID. - [HttpGet, Produces("application/json")] - [Route("log/fetch/{id}")] - public async Task GetAsync(string id) - { - PasteInfo response = await this.Pastebin.GetAsync(id); - response.Content = this.DecompressString(response.Content); - return response; - } - /// Save raw log data. /// The log content to save. [HttpPost, Produces("application/json"), AllowLargePosts] @@ -85,6 +85,15 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Private methods *********/ + /// Fetch raw text from Pastebin. + /// The Pastebin paste ID. + private async Task GetAsync(string id) + { + PasteInfo response = await this.Pastebin.GetAsync(id); + response.Content = this.DecompressString(response.Content); + return response; + } + /// Compress a string. /// The text to compress. /// Derived from . diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs b/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs new file mode 100644 index 00000000..5d4c8c08 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs @@ -0,0 +1,15 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.LogParsing +{ + /// An error while parsing the log file which doesn't require a stack trace to troubleshoot. + internal class LogParseException : Exception + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user-friendly error message. + public LogParseException(string message) : base(message) { } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs new file mode 100644 index 00000000..1c3b5671 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using StardewModdingAPI.Web.Framework.LogParsing.Models; + +namespace StardewModdingAPI.Web.Framework.LogParsing +{ + /// Parses SMAPI log files. + public class LogParser + { + /********* + ** Properties + *********/ + /// A regex pattern matching the start of a SMAPI message. + private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?