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') 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 cf383870837748e83b99bf63d36d7a8709743715 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Feb 2018 23:58:27 -0500 Subject: log mod errors and warnings as the mod (#438) --- docs/release-notes.md | 3 +++ src/SMAPI/Framework/InternalExtensions.cs | 12 ++++++++++++ src/SMAPI/Framework/SContentManager.cs | 14 +++++++------- src/SMAPI/Program.cs | 6 +++--- 4 files changed, 25 insertions(+), 10 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index c7ceb887..c7f5cfe9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,8 @@ # Release notes ## 2.5 +* For players: + * Fixed mod crashes being logged under `[SMAPI]` instead of the mod name. + * For modders: * Fixed error when accessing a mod-provided API whose underlying class is `internal`. diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index bec6c183..0340a92d 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -85,6 +85,18 @@ namespace StardewModdingAPI.Framework } } + /**** + ** IModMetadata + ****/ + /// Log a message using the mod's monitor. + /// The mod whose monitor to use. + /// The message to log. + /// The log severity level. + public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) + { + metadata.Mod.Monitor.Log(message, level); + } + /**** ** Exceptions ****/ diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index ebea6c84..ff227fac 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -581,7 +581,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -608,14 +608,14 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return null; } // validate asset if (data == null) { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); return null; } @@ -644,7 +644,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -657,18 +657,18 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit if (asset.Data == null) { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } else if (!(asset.Data is T)) { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7eda9c66..c7da581d 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -255,7 +255,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"The {mod.DisplayName} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); } } @@ -793,7 +793,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); + metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); } // get mod API @@ -900,7 +900,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}"); } } } -- 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') 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 efd331ccd113fac5ba11825dcd1b2a446a65cdfe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 20:20:28 -0500 Subject: enable update checks for older Entoarox mods per request, update More Animals ID --- docs/release-notes.md | 1 + src/SMAPI/StardewModdingAPI.config.json | 38 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 19 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index dd39a179..6c4bdf94 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.5 * For players: * Fixed mod crashes being logged under `[SMAPI]` instead of the mod name. + * Updated compatibility list and enabled update checks for more older mods. * For modders: * Fixed error when accessing a mod-provided API whose underlying class is `internal`. diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index ca0c20b1..cdf7effb 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -115,7 +115,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Advanced Location Loader "ID": "Entoarox.AdvancedLocationLoader", - //"Default | UpdateKey": "Chucklefish:3619", // Entoarox opted out of mod update checks + "~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 }, { @@ -590,7 +590,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Entoarox Framework "ID": "Entoarox.EntoaroxFramework", "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? - //"Default | UpdateKey": "Chucklefish:4228", // Entoarox opted out of mod update checks + "~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 }, { @@ -627,8 +627,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Extended Minecart "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 + "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 @@ -683,8 +683,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // 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 - // "Default | UpdateKey": "Chucklefish:3641" // Entoarox opted out of mod update checks + "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 @@ -727,8 +727,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Furniture Anywhere "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 + "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 @@ -986,6 +986,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it's no longer maintained or used." }, + { + // 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 "ID": "451", @@ -998,13 +1005,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Platonymous.MoreMapLayers", "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 }, - { - // More Pets - "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": "Omegasis.MoreRain", @@ -1303,7 +1303,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Seasonal Immersion "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.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 }, { @@ -1354,7 +1354,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // 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 - // "Default | UpdateKey": "Chucklefish:4381", // Entoarox opted out of mod update checks + "~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 }, { @@ -1733,8 +1733,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Xnb Loader "ID": "Entoarox.XnbLoader", - // "Default | UpdateKey": "Chucklefish:4506", // Entoarox opted out of mod update checks - "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + "~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 -- cgit From 3fc9b39486f5d95c0fd032ab8b7ded9784d40121 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 20:40:22 -0500 Subject: various updates & fixes in mod list --- src/SMAPI/StardewModdingAPI.config.json | 94 ++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index cdf7effb..7518d6b3 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -132,20 +132,20 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // All Crops All Seasons - "ID": "community.AllCropsAllSeasons", - "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7", // changed in 1.3 + "ID": "cantorsdust.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 "Default | UpdateKey": "Nexus:170" }, { // All Professions - "ID": "community.AllProfessions", - "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3", // changed in 1.2 + "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 "ID": "439", - "FormerIDs": "{EntryDLL: 'AlmightyTool.dll'}", // changed in 1.2.1 + "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 @@ -159,7 +159,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Animal Sitter "ID": "jwdred.AnimalSitter", - "FormerIDs": "{EntryDLL: 'AnimalSitter.dll'}", // changed in 1.0.9 + "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 "Default | UpdateKey": "Nexus:581", "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -331,7 +331,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Chest Pooling "ID": "mralbobo.ChestPooling", - "FormerIDs": "{EntryDLL: 'ChestPooling.dll'}", // changed in 1.3 + "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, @@ -344,7 +344,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Choose Baby Gender - "ID": "ChooseBabyGender.dll", + "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" @@ -441,7 +441,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // CrabNet "ID": "jwdred.CrabNet", - "FormerIDs": "{EntryDLL: 'CrabNet.dll'}", // changed in 1.0.5 + "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 "Default | UpdateKey": "Nexus:584", "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -644,13 +644,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Farm Automation: Barn Door Automation - "ID": "FarmAutomation.BarnDoorAutomation.dll", + "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 - "ID": "FarmAutomation.ItemCollector.dll", + "ID": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, @@ -670,7 +670,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Farm Resource Generator - "ID": "FarmResourceGenerator.dll", + "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" @@ -689,7 +689,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Faster Run "ID": "KathrynHazuka.FasterRun", - "FormerIDs": "{EntryDLL: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update + "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" @@ -706,7 +706,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // FlorenceMod - "ID": "FlorenceMod.dll", + "ID": "{EntryDll: 'FlorenceMod.dll'}", "MapLocalVersions": { "1.0.1": "1.1" }, "Default | UpdateKey": "Nexus:591", "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 @@ -738,7 +738,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Gate Opener "ID": "mralbobo.GateOpener", - "FormerIDs": "{EntryDLL: 'GateOpener.dll'}", // changed in 1.1 + "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 }, @@ -756,7 +756,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Get Dressed "ID": "Advize.GetDressed", - "FormerIDs": "{EntryDLL: 'GetDressed.dll'}", // changed in 3.3 + "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 "Default | UpdateKey": "Nexus:331", "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 }, @@ -855,8 +855,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Instant Grow Trees - "ID": "community.InstantGrowTrees", - "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59", // changed in 1.2 + "ID": "cantorsdust.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 "Default | UpdateKey": "Nexus:173" }, { @@ -875,7 +875,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Jiggly Junimo Bundles "ID": "Greger.JigglyJunimoBundles", - "FormerIDs": "{EntryDLL: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update + "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update }, { @@ -927,7 +927,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Loved Labels - "ID": "LovedLabels.dll", + "ID": "Advize.LovedLabels", + "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 "Default | UpdateKey": "Nexus:279", "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -946,7 +947,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // MailOrderPigs "ID": "jwdred.MailOrderPigs", - "FormerIDs": "{EntryDLL: 'MailOrderPigs.dll'}", // changed in 1.0.2 + "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 "Default | UpdateKey": "Nexus:632", "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -1095,7 +1096,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // NPC Speak - "ID": "NpcEcho.dll", + "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" @@ -1126,7 +1127,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // PelicanFiber "ID": "jwdred.PelicanFiber", - "FormerIDs": "{EntryDLL: 'PelicanFiber.dll'}", // changed in 3.0.1 + "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 @@ -1145,7 +1146,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Persival's BundleMod - "ID": "BundleMod.dll", + "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" @@ -1158,7 +1159,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Point-and-Plant "ID": "jwdred.PointAndPlant", - "FormerIDs": "{EntryDLL: 'PointAndPlant.dll'}", // changed in 1.0.3 + "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 "Default | UpdateKey": "Nexus:572", "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, @@ -1175,7 +1176,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Prairie King Made Easy "ID": "Mucchan.PrairieKingMadeEasy", - "FormerIDs": "{EntryDLL: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 + "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" @@ -1187,14 +1188,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Rain Randomizer - "ID": "RainRandomizer.dll", + "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 - "ID": "community.RecatchLegendaryFish", - "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9", // changed in 1.3 + "ID": "cantorsdust.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 "Default | UpdateKey": "Nexus:172" }, { @@ -1230,7 +1231,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Replanter "ID": "jwdred.Replanter", - "FormerIDs": "{EntryDLL: 'Replanter.dll'}", // changed in 1.0.5 + "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 "Default | UpdateKey": "Nexus:589", "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -1379,13 +1380,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Simple Sound Manager "ID": "Omegasis.SimpleSoundManager", "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 + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, { // Simple Sprinklers "ID": "tZed.SimpleSprinkler", - "FormerIDs": "{EntryDLL: 'SimpleSprinkler.dll'}", // changed in 1.5 + "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 "Default | UpdateKey": "Nexus:76", "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 }, @@ -1425,8 +1425,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Skull Cave Saver - "ID": "community.SkullCaveSaver", - "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774", // changed in 1.1 + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 "Default | UpdateKey": "Nexus:175" }, { @@ -1502,13 +1502,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // StackSplitX "ID": "tstaples.StackSplitX", - "FormerIDs": "{EntryDLL: 'StackSplitX.dll'}", // changed circa 1.3.1 + "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", + "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" }, @@ -1569,7 +1569,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Stone Bridge Over Pond (PondWithBridge) - "ID": "PondWithBridge.dll", + "ID": "{EntryDll: 'PondWithBridge.dll'}", "MapLocalVersions": { "0.0": "1.0" }, "Default | UpdateKey": "Nexus:316", "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 @@ -1593,7 +1593,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Tainted Cellar - "ID": "TaintedCellar.dll", + "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" }, @@ -1639,8 +1639,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // TimeSpeed - "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 + "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 }, @@ -1686,13 +1686,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // WakeUp - "ID": "WakeUp.dll", + "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 - "ID": "WallpaperFix.dll", + "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" @@ -1704,7 +1704,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Weather Controller - "ID": "WeatherController.dll", + "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" }, @@ -1720,13 +1720,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Wonderful Farm Life - "ID": "WonderfulFarmLife.dll", + "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 - "ID": "XmlSerializerRetool.dll", + "ID": "{EntryDll: 'XmlSerializerRetool.dll'}", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it's no longer maintained or used." }, @@ -1773,7 +1773,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Health Bars "ID": "Zoryn.HealthBars", - "FormerIDs": "{EntryDLL: 'HealthBars.dll'}", // changed in 1.6 + "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 }, -- 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') 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 789b2f4e42f36aa55dfe1f3160259e540addbc2f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 22:17:39 -0500 Subject: add common dependencies to mod data for display names (#439) --- src/SMAPI/StardewModdingAPI.config.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 8b92f277..ff5b1f9d 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -152,7 +152,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Animal Husbandry": { - "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "ID": "DIGUS.ANIMALHUSBANDRYMOD", "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 "Default | UpdateKey": "Nexus:1538" }, @@ -457,12 +457,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1255" }, + "Custom Crops": { + "ID": "spacechase0.CustomCrops" + }, + "Custom Element Handler": { "ID": "Platonymous.CustomElementHandler", "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 }, - "Custom Farming": { + "Custom Farming Redux": { "ID": "Platonymous.CustomFarming", "Default | UpdateKey": "Nexus:991" // added in 0.6.1 }, @@ -510,6 +514,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1027" }, + "Custom NPC": { + "ID": "Platonymous.CustomNPC" + }, + "Custom Shops Redux": { "ID": "Omegasis.CustomShopReduxGui", "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 @@ -881,6 +889,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update }, + "Json Assets": { + "ID": "spacechase0.JsonAssets" + }, + "Junimo Farm": { "ID": "Platonymous.JunimoFarm", "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated @@ -1159,6 +1171,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1026" }, + "PyTK - Platonymous Toolkit": { + "ID": "Platonymous.Toolkit" + }, + "Point-and-Plant": { "ID": "jwdred.PointAndPlant", "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 -- cgit From d926133608b227add19f0aa711bf3efb5da5f0bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 22:33:33 -0500 Subject: fix deadlock in rare cases when injecting an asset (#441) --- docs/release-notes.md | 1 + src/SMAPI/Framework/SContentManager.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index c4c269eb..6b0c5d28 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * 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. diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index ff227fac..463fea0b 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -792,12 +792,12 @@ namespace StardewModdingAPI.Framework { try { - this.Lock.EnterReadLock(); + this.Lock.EnterWriteLock(); return action(); } finally { - this.Lock.ExitReadLock(); + this.Lock.ExitWriteLock(); } } } -- 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') 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') 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 4d9f8368169c39d05d43ca2258a4757af2e85480 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Feb 2018 23:12:22 -0500 Subject: update Nexus URLs --- src/SMAPI/Constants.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index caee93e4..615f45b9 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -25,8 +25,8 @@ namespace StardewModdingAPI 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" + ["GitHub"] = "https://github.com/{0}/releases", + ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" }; -- cgit From 754e356adc8fa23f4cecef588406f126a3e1a4a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 02:00:46 -0500 Subject: add install scripts for Linux/Mac (#434) --- build/prepare-install-package.targets | 3 +- docs/release-notes.md | 1 + .../StardewModdingAPI.Installer.csproj | 8 ++ src/SMAPI.Installer/unix-install.sh | 21 +++++ src/SMAPI.Installer/unix-launcher.sh | 95 ++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 3 - src/SMAPI/unix-launcher.sh | 95 ---------------------- 7 files changed, 127 insertions(+), 99 deletions(-) create mode 100644 src/SMAPI.Installer/unix-install.sh create mode 100644 src/SMAPI.Installer/unix-launcher.sh delete mode 100644 src/SMAPI/unix-launcher.sh (limited to 'src/SMAPI') diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index f2a2b23c..fca311f2 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -20,6 +20,8 @@ + + @@ -31,7 +33,6 @@ - diff --git a/docs/release-notes.md b/docs/release-notes.md index 51f0b76c..f44db00c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ # Release notes ## 2.5 * For players: + * Added install scripts for Linux/Mac (no longer need to run `mono install.exe` through the terminal). * 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. * Fixed uninstall script not confirming success on Linux/Mac. diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index d3a6aa0b..a575e06f 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -50,6 +50,14 @@ Always + + + PreserveNewest + + + PreserveNewest + + diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh new file mode 100644 index 00000000..a0bd9346 --- /dev/null +++ b/src/SMAPI.Installer/unix-install.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Run the SMAPI installer through Mono on Linux or Mac. + +# Move to script's directory +cd "`dirname "$0"`" + +# get cross-distro version of POSIX command +COMMAND="" +if command -v command >/dev/null 2>&1; then + COMMAND="command -v" +elif type type >/dev/null 2>&1; then + COMMAND="type" +fi + +# validate Mono & run installer +if $COMMAND mono >/dev/null 2>&1; then + mono install.exe +else + echo "Oops! Looks like Mono isn't installed. Please install Mono from http://mono-project.com, reboot, and run this installer again." + read +fi diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh new file mode 100644 index 00000000..2542a286 --- /dev/null +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# MonoKickstart Shell Script +# Written by Ethan "flibitijibibo" Lee +# Modified for StardewModdingAPI by Viz and Pathoschild + +# Move to script's directory +cd "`dirname "$0"`" + +# Get the system architecture +UNAME=`uname` +ARCH=`uname -m` + +# MonoKickstart picks the right libfolder, so just execute the right binary. +if [ "$UNAME" == "Darwin" ]; then + # ... Except on OSX. + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ + + # El Capitan is a total idiot and wipes this variable out, making the + # Steam overlay disappear. This sidesteps "System Integrity Protection" + # and resets the variable with Valve's own variable (they provided this + # fix by the way, thanks Valve!). Note that you will need to update your + # launch configuration to the script location, NOT just the app location + # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). + # -flibit + if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then + export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" + fi + + # this was here before + ln -sf mcs.bin.osx mcs + + # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI + if [ -f libgdiplus.dylib ]; then + rm libgdiplus.dylib + fi + if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then + ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib + fi + + # launch SMAPI + cp StardewValley.bin.osx StardewModdingAPI.bin.osx + open -a Terminal ./StardewModdingAPI.bin.osx $@ +else + # choose launcher + LAUNCHER="" + if [ "$ARCH" == "x86_64" ]; then + ln -sf mcs.bin.x86_64 mcs + cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 + LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" + else + ln -sf mcs.bin.x86 mcs + cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 + LAUNCHER="./StardewModdingAPI.bin.x86 $@" + fi + + # get cross-distro version of POSIX command + COMMAND="" + if command -v command 2>/dev/null; then + COMMAND="command -v" + elif type type 2>/dev/null; then + COMMAND="type" + fi + + # open SMAPI in terminal + if $COMMAND x-terminal-emulator 2>/dev/null; then + # Terminator converts -e to -x when used through x-terminal-emulator for some reason (per + # `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator + # is mapped to Terminator, invoke it directly instead. + if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then + terminator -e "$LAUNCHER" + else + x-terminal-emulator -e "$LAUNCHER" + fi + elif $COMMAND xfce4-terminal 2>/dev/null; then + xfce4-terminal -e "$LAUNCHER" + elif $COMMAND gnome-terminal 2>/dev/null; then + gnome-terminal -e "$LAUNCHER" + elif $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" + elif $COMMAND konsole 2>/dev/null; then + konsole -e "$LAUNCHER" + elif $COMMAND terminal 2>/dev/null; then + terminal -e "$LAUNCHER" + else + $LAUNCHER + fi + + # some Linux users get error 127 (command not found) from the above block, even though + # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when + # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal + # (which can be slow if there is none). + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi +fi diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index e181c435..ab247f05 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -250,9 +250,6 @@ Always - - Always - diff --git a/src/SMAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh deleted file mode 100644 index 2542a286..00000000 --- a/src/SMAPI/unix-launcher.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -# MonoKickstart Shell Script -# Written by Ethan "flibitijibibo" Lee -# Modified for StardewModdingAPI by Viz and Pathoschild - -# Move to script's directory -cd "`dirname "$0"`" - -# Get the system architecture -UNAME=`uname` -ARCH=`uname -m` - -# MonoKickstart picks the right libfolder, so just execute the right binary. -if [ "$UNAME" == "Darwin" ]; then - # ... Except on OSX. - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ - - # El Capitan is a total idiot and wipes this variable out, making the - # Steam overlay disappear. This sidesteps "System Integrity Protection" - # and resets the variable with Valve's own variable (they provided this - # fix by the way, thanks Valve!). Note that you will need to update your - # launch configuration to the script location, NOT just the app location - # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). - # -flibit - if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then - export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" - fi - - # this was here before - ln -sf mcs.bin.osx mcs - - # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI - if [ -f libgdiplus.dylib ]; then - rm libgdiplus.dylib - fi - if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then - ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib - fi - - # launch SMAPI - cp StardewValley.bin.osx StardewModdingAPI.bin.osx - open -a Terminal ./StardewModdingAPI.bin.osx $@ -else - # choose launcher - LAUNCHER="" - if [ "$ARCH" == "x86_64" ]; then - ln -sf mcs.bin.x86_64 mcs - cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" - else - ln -sf mcs.bin.x86 mcs - cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86 $@" - fi - - # get cross-distro version of POSIX command - COMMAND="" - if command -v command 2>/dev/null; then - COMMAND="command -v" - elif type type 2>/dev/null; then - COMMAND="type" - fi - - # open SMAPI in terminal - if $COMMAND x-terminal-emulator 2>/dev/null; then - # Terminator converts -e to -x when used through x-terminal-emulator for some reason (per - # `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator - # is mapped to Terminator, invoke it directly instead. - if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then - terminator -e "$LAUNCHER" - else - x-terminal-emulator -e "$LAUNCHER" - fi - elif $COMMAND xfce4-terminal 2>/dev/null; then - xfce4-terminal -e "$LAUNCHER" - elif $COMMAND gnome-terminal 2>/dev/null; then - gnome-terminal -e "$LAUNCHER" - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" - elif $COMMAND konsole 2>/dev/null; then - konsole -e "$LAUNCHER" - elif $COMMAND terminal 2>/dev/null; then - terminal -e "$LAUNCHER" - else - $LAUNCHER - fi - - # some Linux users get error 127 (command not found) from the above block, even though - # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when - # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal - # (which can be slow if there is none). - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi -fi -- 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') 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 84330e86809dff4a3e70c3cbd59f0373f7017799 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 18:43:19 -0500 Subject: split proxy builder & factory (#435) --- .../Framework/ModHelpers/ModRegistryHelper.cs | 10 +-- .../Framework/Reflection/InterfaceProxyBuilder.cs | 92 +++++++++------------- .../Framework/Reflection/InterfaceProxyFactory.cs | 58 ++++++++++++++ src/SMAPI/Program.cs | 4 +- src/SMAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index ea0dbb38..e579a830 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private readonly HashSet AccessedModApis = new HashSet(); /// Generates proxy classes to access mod APIs through an arbitrary interface. - private readonly InterfaceProxyBuilder ProxyBuilder; + private readonly InterfaceProxyFactory ProxyFactory; /********* @@ -29,13 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The unique ID of the relevant mod. /// The underlying mod registry. - /// Generates proxy classes to access mod APIs through an arbitrary interface. + /// Generates proxy classes to access mod APIs through an arbitrary interface. /// Encapsulates monitoring and logging for the mod. - public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor) + public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) : base(modID) { this.Registry = registry; - this.ProxyBuilder = proxyBuilder; + this.ProxyFactory = proxyFactory; this.Monitor = monitor; } @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get API of type if (api is TInterface castApi) return castApi; - return this.ProxyBuilder.CreateProxy(api, this.ModID, uniqueID); + return this.ProxyFactory.CreateProxy(api, this.ModID, uniqueID); } } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 5abebc18..7a2958fb 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -1,82 +1,47 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; namespace StardewModdingAPI.Framework.Reflection { - /// Generates proxy classes to access mod APIs through an arbitrary interface. + /// Generates a proxy class to access a mod API through an arbitrary interface. internal class InterfaceProxyBuilder { /********* ** Properties *********/ - /// The CLR module in which to create proxy classes. - private readonly ModuleBuilder ModuleBuilder; + /// The target class type. + private readonly Type TargetType; - /// The generated proxy types. - private readonly IDictionary GeneratedTypes = new Dictionary(); + /// The generated proxy type. + private readonly Type ProxyType; /********* ** Public methods *********/ /// Construct an instance. - public InterfaceProxyBuilder() - { - AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); - this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); - } - - /// Create an API proxy. - /// The interface through which to access the API. - /// The API instance to access. - /// The unique ID of the mod consuming the API. - /// The unique ID of the mod providing the API. - public TInterface CreateProxy(object instance, string sourceModID, string targetModID) - where TInterface : class + /// The type name to generate. + /// The CLR module in which to create proxy classes. + /// The interface type to implement. + /// The target type. + public InterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) { // validate - if (instance == null) - throw new InvalidOperationException("Can't proxy access to a null API."); - if (!typeof(TInterface).IsInterface) - throw new InvalidOperationException("The proxy type must be an interface, not a class."); - - // get proxy type - Type targetType = instance.GetType(); - string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; - if (!this.GeneratedTypes.TryGetValue(proxyTypeName, out Type type)) - { - type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType); - this.GeneratedTypes[proxyTypeName] = type; - } + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); - // create instance - ConstructorInfo constructor = type.GetConstructor(new[] { targetType }); - if (constructor == null) - throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen - return (TInterface)constructor.Invoke(new[] { instance }); - } - - - /********* - ** Private methods - *********/ - /// Define a class which proxies access to a target type through an interface. - /// The name of the proxy type to generate. - /// The interface type through which to access the target. - /// The target type to access. - private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType) - { // define proxy type - TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class); + TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); proxyBuilder.AddInterfaceImplementation(interfaceType); // create field to store target instance - FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); + FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); - // create constructor which accepts target instance + // create constructor which accepts target instance and sets field { ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); ILGenerator il = constructor.GetILGenerator(); @@ -86,7 +51,7 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_1); // load argument - il.Emit(OpCodes.Stfld, field); // set field to loaded argument + il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument il.Emit(OpCodes.Ret); } @@ -97,13 +62,28 @@ namespace StardewModdingAPI.Framework.Reflection if (targetMethod == null) throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); - this.ProxyMethod(proxyBuilder, targetMethod, field); + this.ProxyMethod(proxyBuilder, targetMethod, targetField); } - // create type - return proxyBuilder.CreateType(); + // save info + this.TargetType = targetType; + this.ProxyType = proxyBuilder.CreateType(); + } + + /// Create an instance of the proxy for a target instance. + /// The target instance. + public object CreateInstance(object targetInstance) + { + ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); + if (constructor == null) + throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen + return constructor.Invoke(new[] { targetInstance }); } + + /********* + ** Private methods + *********/ /// Define a method which proxies access to a method on the target. /// The proxy type being generated. /// The target method. diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs new file mode 100644 index 00000000..e14a9f08 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Generates proxy classes to access mod APIs through an arbitrary interface. + internal class InterfaceProxyFactory + { + /********* + ** Properties + *********/ + /// The CLR module in which to create proxy classes. + private readonly ModuleBuilder ModuleBuilder; + + /// The generated proxy types. + private readonly IDictionary Builders = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public InterfaceProxyFactory() + { + AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); + } + + /// Create an API proxy. + /// The interface through which to access the API. + /// The API instance to access. + /// The unique ID of the mod consuming the API. + /// The unique ID of the mod providing the API. + public TInterface CreateProxy(object instance, string sourceModID, string targetModID) + where TInterface : class + { + // validate + if (instance == null) + throw new InvalidOperationException("Can't proxy access to a null API."); + if (!typeof(TInterface).IsInterface) + throw new InvalidOperationException("The proxy type must be an interface, not a class."); + + // get proxy type + Type targetType = instance.GetType(); + string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; + if (!this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder)) + { + builder = new InterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType); + this.Builders[proxyTypeName] = builder; + } + + // create instance + return (TInterface)builder.CreateInstance(instance); + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 275876dd..88e27768 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -659,7 +659,7 @@ namespace StardewModdingAPI AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); - InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder(); + InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); foreach (IModMetadata metadata in mods) { // get basic info @@ -711,7 +711,7 @@ namespace StardewModdingAPI ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor); + 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); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index ab247f05..eb403309 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -113,6 +113,7 @@ + -- cgit From 0c1bca3db044b6f228538f1738d52c31e4481e48 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 17 Feb 2018 18:51:09 -0500 Subject: validate that mod APIs are public (#435) --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index 228fec82..f0a7a718 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ * For modders: * Fixed deadlock in rare cases when injecting a file with an asset loader. + * Fixed unhelpful error when a mod exposes a non-public API. * For SMAPI developers: * Overhauled `StardewModdingApi.config.json`'s `ModData` format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 88e27768..fd2bb340 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -804,6 +804,12 @@ namespace StardewModdingAPI try { object api = metadata.Mod.GetApi(); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } + if (api != null) this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); metadata.SetApi(api); -- 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') diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs new file mode 100644 index 00000000..0a8f223e --- /dev/null +++ b/src/SMAPI/Framework/ContentPack.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Serialisation; +using xTile; + +namespace StardewModdingAPI.Framework +{ + /// Manages access to a content pack's metadata and files. + internal class ContentPack : IContentPack + { + /********* + ** Properties + *********/ + /// Provides an API for loading content assets. + private readonly IContentHelper Content; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + + /********* + ** Accessors + *********/ + /// The full path to the content pack's folder. + public string DirectoryPath { get; } + + /// The content pack's manifest. + public IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full path to the content pack's folder. + /// The content pack's manifest. + /// Provides an API for loading content assets. + /// Encapsulates SMAPI's JSON file parsing. + public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, JsonHelper jsonHelper) + { + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Content = content; + this.JsonHelper = jsonHelper; + } + + /// Read a JSON file from the content pack folder. + /// The model type. + /// The file path relative to the contnet directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + public TModel ReadJsonFile(string path) where TModel : class + { + path = Path.Combine(this.DirectoryPath, path); + return this.JsonHelper.ReadJsonFile(path); + } + + /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T LoadAsset(string key) + { + return this.Content.Load(key, ContentSource.ModFolder); + } + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The the local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + public string GetActualAssetKey(string key) + { + return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); + } + + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index a91b0a5b..d1e8eb7d 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework /// The mod manifest. IManifest Manifest { get; } - /// >Metadata about the mod from SMAPI's internal data (if any). + /// Metadata about the mod from SMAPI's internal data (if any). ParsedModDataRecord DataRecord { get; } /// The metadata resolution status. @@ -27,12 +27,21 @@ namespace StardewModdingAPI.Framework /// The reason the metadata is invalid, if any. string Error { get; } - /// The mod instance (if it was loaded). + /// The mod instance (if loaded and is false). IMod Mod { get; } + /// The content pack instance (if loaded and is true). + IContentPack ContentPack { get; } + + /// Writes messages to the console and log file as this mod. + IMonitor Monitor { get; } + /// The mod-provided API (if any). object Api { get; } + /// Whether the mod is a content pack. + bool IsContentPack { get; } + /********* ** Public methods @@ -47,6 +56,11 @@ namespace StardewModdingAPI.Framework /// The mod instance to set. IModMetadata SetMod(IMod mod); + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + IModMetadata SetMod(IContentPack contentPack, IMonitor monitor); + /// Set the mod-provided API instance. /// The mod-provided API. IModMetadata SetApi(object api); diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 0340a92d..71489627 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) { - metadata.Mod.Monitor.Log(message, level); + metadata.Monitor.Log(message, level); } /**** diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 665b9cf4..c73dc307 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,5 +1,7 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.ModHelpers @@ -13,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// The content packs loaded for this mod. + private readonly IContentPack[] ContentPacks; + /********* ** Accessors @@ -48,9 +53,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /// an API for fetching metadata about loaded mods. /// An API for accessing private game code. /// An API for reading translations stored in the mod's i18n folder. + /// The content packs loaded for this mod. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable contentPacks) : base(modID) { // validate directory @@ -67,6 +73,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); + this.ContentPacks = contentPacks.ToArray(); } /**** @@ -116,6 +123,14 @@ namespace StardewModdingAPI.Framework.ModHelpers this.JsonHelper.WriteJsonFile(path, model); } + /**** + ** Content packs + ****/ + /// Get all content packs loaded for this mod. + public IEnumerable GetContentPacks() + { + return this.ContentPacks; + } /**** ** Disposal diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 29bb6617..1a0f9994 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading @@ -26,12 +27,21 @@ namespace StardewModdingAPI.Framework.ModLoading /// The reason the metadata is invalid, if any. public string Error { get; private set; } - /// The mod instance (if it was loaded). + /// The mod instance (if loaded and is false). public IMod Mod { get; private set; } + /// The content pack instance (if loaded and is true). + public IContentPack ContentPack { get; private set; } + + /// Writes messages to the console and log file as this mod. + public IMonitor Monitor { get; private set; } + /// The mod-provided API (if any). public object Api { get; private set; } + /// Whether the mod is a content pack. + public bool IsContentPack => this.Manifest?.ContentPackFor != null; + /********* ** Public methods @@ -64,7 +74,24 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod instance to set. public IModMetadata SetMod(IMod mod) { + if (this.ContentPack != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + this.Mod = mod; + this.Monitor = mod.Monitor; + return this; + } + + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor) + { + if (this.Mod != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + + this.ContentPack = contentPack; + this.Monitor = monitor; return this; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b46ee117..be73254d 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -30,18 +30,13 @@ namespace StardewModdingAPI.Framework.ModLoading string error = null; try { - // read manifest manifest = jsonHelper.ReadJsonFile(path); - - // validate if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - error = "its manifest doesn't set an entry DLL."; } catch (SParseException ex) { @@ -85,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; - // validate compatibility + // validate compatibility from internal data switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: @@ -128,24 +123,52 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL value - if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll)) - { - mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field."); - continue; - } - if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + // validate DLL / content pack fields { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } + bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); + bool isContentPack = mod.Manifest.ContentPackFor != null; - // validate DLL path - string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; + // validate field presence + if (!hasDll && !isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + continue; + } + if (hasDll && isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + continue; + } + + // validate DLL + if (hasDll) + { + // invalid filename format + if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + continue; + } + + // invalid path + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); + if (!File.Exists(assemblyPath)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + } + + // validate content pack + else + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + continue; + } + } } // validate required fields @@ -243,30 +266,17 @@ namespace StardewModdingAPI.Framework.ModLoading throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } - // no dependencies, mark sorted - if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) + // collect dependencies + ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + + // mark sorted if no dependencies + if (!dependencies.Any()) { sortedMods.Push(mod); return states[mod] = ModDependencyStatus.Sorted; } - // get dependencies - var dependencies = - ( - from entry in mod.Manifest.Dependencies - let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - orderby entry.UniqueID - select new - { - ID = entry.UniqueID, - MinVersion = entry.MinimumVersion, - Mod = dependencyMod, - IsRequired = entry.IsRequired - } - ) - .ToArray(); - - // missing required dependencies, mark failed + // mark failed if missing dependencies { string[] failedModNames = ( from entry in dependencies @@ -371,5 +381,64 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } + + /// Get the dependencies declared in a manifest. + /// The mod manifest. + /// The loaded mods. + private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + { + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + + // yield dependencies + if (manifest.Dependencies != null) + { + foreach (var entry in manifest.Dependencies) + yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired); + } + + // yield content pack parent + if (manifest.ContentPackFor != null) + yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true); + } + + + /********* + ** Private models + *********/ + /// Represents a dependency from one mod to another. + private struct ModDependency + { + /********* + ** Accessors + *********/ + /// The unique ID of the required mod. + public string ID { get; } + + /// The minimum required version (if any). + public ISemanticVersion MinVersion { get; } + + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public bool IsRequired { get; } + + /// The loaded mod that fulfills the dependency (if available). + public IModMetadata Mod { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the required mod. + /// The minimum required version (if any). + /// The loaded mod that fulfills the dependency (if available). + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired) + { + this.ID = id; + this.MinVersion = minVersion; + this.Mod = mod; + this.IsRequired = isRequired; + } + } } } diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 453d2868..e7d4f89a 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -25,18 +25,27 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ - /// Register a mod as a possible source of deprecation warnings. + /// Register a mod. /// The mod metadata. public void Add(IModMetadata metadata) { this.Mods.Add(metadata); - this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; + if (!metadata.IsContentPack) + this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; } /// Get metadata for all loaded mods. - public IEnumerable GetAll() + /// Whether to include SMAPI mods. + /// Whether to include content pack mods. + public IEnumerable GetAll(bool assemblyMods = true, bool contentPacks = true) { - return this.Mods.Select(p => p); + IEnumerable query = this.Mods; + if (!assemblyMods) + query = query.Where(p => p.IsContentPack); + if (!contentPacks) + query = query.Where(p => !p.IsContentPack); + + return query; } /// Get metadata for a loaded mod. diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index f9762406..74303cba 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -27,9 +27,13 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. Mutually exclusive with . public string EntryDll { get; set; } + /// The mod which will read this as a content pack. Mutually exclusive with . + [JsonConverter(typeof(ManifestContentPackForConverter))] + public IManifestContentPackFor ContentPackFor { get; set; } + /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestDependencyArrayConverter))] public IManifestDependency[] Dependencies { get; set; } diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..7836bbcc --- /dev/null +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + internal class ManifestContentPackFor : IManifestContentPackFor + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod which can read this content pack. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs new file mode 100644 index 00000000..af7558f6 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters +{ + /// Handles deserialisation of arrays. + internal class ManifestContentPackForConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IManifestContentPackFor[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs new file mode 100644 index 00000000..15a2b7dd --- /dev/null +++ b/src/SMAPI/IContentPack.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using xTile; + +namespace StardewModdingAPI +{ + /// An API that provides access to a content pack. + public interface IContentPack + { + /********* + ** Accessors + *********/ + /// The full path to the content pack's folder. + string DirectoryPath { get; } + + /// The content pack's manifest. + IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// Read a JSON file from the content pack folder. + /// The model type. + /// The file path relative to the content pack directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + TModel ReadJsonFile(string path) where TModel : class; + + /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T LoadAsset(string key); + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The the local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + string GetActualAssetKey(string key); + } +} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs index 9db1d538..183ac105 100644 --- a/src/SMAPI/IManifest.cs +++ b/src/SMAPI/IManifest.cs @@ -26,9 +26,12 @@ namespace StardewModdingAPI /// The unique mod ID. string UniqueID { get; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. Mutually exclusive with . string EntryDll { get; } + /// The mod which will read this as a content pack. Mutually exclusive with . + IManifestContentPackFor ContentPackFor { get; } + /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } diff --git a/src/SMAPI/IManifestContentPackFor.cs b/src/SMAPI/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/SMAPI/IManifestContentPackFor.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public interface IManifestContentPackFor + { + /// The unique ID of the mod which can read this content pack. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + } +} diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 116e8508..96265c85 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,4 +1,6 @@ -namespace StardewModdingAPI +using System.Collections.Generic; + +namespace StardewModdingAPI { /// Provides simplified APIs for writing mods. public interface IModHelper @@ -54,5 +56,11 @@ /// The file path relative to the mod directory. /// The model to save. void WriteJsonFile(string path, TModel model) where TModel : class; + + /**** + ** Content packs + ****/ + /// Get all content packs loaded for this mod. + IEnumerable GetContentPacks(); } -} \ No newline at end of file +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index fd2bb340..e0064714 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -394,7 +394,7 @@ namespace StardewModdingAPI LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); } @@ -652,15 +652,52 @@ namespace StardewModdingAPI { this.Monitor.Log("Loading mods...", LogLevel.Trace); - // load mod assemblies IDictionary skippedMods = new Dictionary(); + void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + + // load content packs + foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) + { + // get basic info + IManifest manifest = metadata.Manifest; + this.Monitor.Log($"Loading {metadata.DisplayName} from {metadata.DirectoryPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)} (content pack)...", LogLevel.Trace); + + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } + + // load mod as content pack + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); + metadata.SetMod(contentPack, monitor); + this.ModRegistry.Add(metadata); + } + IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); + + // load mods { - void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + // get content packs by mod ID + IDictionary contentPacksByModID = + loadedContentPacks + .GroupBy(p => p.Manifest.ContentPackFor.UniqueID) + .ToDictionary( + group => group.Key, + group => group.Select(metadata => metadata.ContentPack).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + // get assembly loaders AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - foreach (IModMetadata metadata in mods) + + // load from metadata + foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) { // get basic info IManifest manifest = metadata.Manifest; @@ -676,7 +713,7 @@ namespace StardewModdingAPI continue; } - // preprocess & load mod assembly + // load mod string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) : null; @@ -704,6 +741,10 @@ namespace StardewModdingAPI // initialise mod try { + // get content packs + if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) + contentPacks = new IContentPack[0]; + // init mod helpers IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IModHelper modHelper; @@ -713,7 +754,7 @@ namespace StardewModdingAPI IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks); } // get mod instance @@ -735,7 +776,7 @@ namespace StardewModdingAPI } } } - IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); // log skipped mods this.Monitor.Newline(); @@ -757,6 +798,7 @@ namespace StardewModdingAPI // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; @@ -769,10 +811,30 @@ namespace StardewModdingAPI } this.Monitor.Newline(); + // log loaded content packs + if (loadedContentPacks.Any()) + { + string GetModDisplayName(string id) => loadedMods.First(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + + this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); + foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (metadata.IsContentPack ? $" | content pack for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + } + // initialise translations - this.ReloadTranslations(); + this.ReloadTranslations(loadedMods); - // initialise loaded mods + // initialise loaded non-content-pack mods foreach (IModMetadata metadata in loadedMods) { // add interceptors @@ -891,11 +953,15 @@ namespace StardewModdingAPI } /// Reload translations for all mods. - private void ReloadTranslations() + /// The mods for which to reload translations. + private void ReloadTranslations(IEnumerable mods) { JsonHelper jsonHelper = new JsonHelper(); - foreach (IModMetadata metadata in this.ModRegistry.GetAll()) + foreach (IModMetadata metadata in mods) { + if (metadata.IsContentPack) + throw new InvalidOperationException("Can't reload translations for a content pack."); + // read translation files IDictionary> translations = new Dictionary>(); DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); @@ -954,7 +1020,7 @@ namespace StardewModdingAPI break; case "reload_i18n": - this.ReloadTranslations(); + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index eb403309..7cf62a91 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -93,6 +94,7 @@ + @@ -114,6 +116,7 @@ + @@ -121,6 +124,8 @@ + + @@ -279,4 +284,4 @@ - \ No newline at end of file + -- cgit From f1c24e30522499199cbf2f75cb68d7b4e5942bf3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Feb 2018 02:31:39 -0500 Subject: add support for ISemanticVersion in JSON models --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/Manifest.cs | 2 -- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index f0a7a718..a1fb4a9d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Updated compatibility list and enabled update checks for more old mods. * For modders: + * Added support for `ISemanticVersion` in JSON models. * Fixed deadlock in rare cases when injecting a file with an asset loader. * Fixed unhelpful error when a mod exposes a non-public API. diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index 74303cba..f5867cf3 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -20,11 +20,9 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// The mod version. - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the method. Mutually exclusive with . diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 2e2a666e..6cba343e 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -21,6 +21,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { + // SMAPI types + new SemanticVersionConverter(), + // enums new StringEnumConverter(), new StringEnumConverter(), -- cgit From 033015066650d4bd67a7df0a7f7addf4c6edf617 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Feb 2018 22:40:20 -0500 Subject: suppress keyboard events when a textbox is focused (#445) --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index 723cc540..2840798d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * Added support for `ISemanticVersion` in JSON models. * Fixed deadlock in rare cases when injecting a file with an asset loader. * Fixed unhelpful error when a mod exposes a non-public API. + * Fixed input events being raised for keyboard buttons when a textbox is receiving input. * For SMAPI developers: * Overhauled mod DB format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index e82ee778..f080f3c5 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -256,7 +256,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Context: before save creation.", LogLevel.Trace); SaveEvents.InvokeBeforeCreate(this.Monitor); } - + // raise before-save if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { @@ -386,20 +386,23 @@ namespace StardewModdingAPI.Framework } // raise input events + bool isTextboxReceiving = Game1.keyboardDispatcher?.Subscriber?.Selected == true; foreach (var pair in inputState.ActiveButtons) { SButton button = pair.Key; InputStatus status = pair.Value; + bool isKeyboard = button.TryGetKeyboard(out Keys keyboardKey); if (status == InputStatus.Pressed) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + if (!isTextboxReceiving || !isKeyboard) + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); // legacy events - if (button.TryGetKeyboard(out Keys key)) + if (isKeyboard) { - if (key != Keys.None) - ControlEvents.InvokeKeyPressed(this.Monitor, key); + if (!isTextboxReceiving && keyboardKey != Keys.None) + ControlEvents.InvokeKeyPressed(this.Monitor, keyboardKey); } else if (button.TryGetController(out Buttons controllerButton)) { @@ -411,13 +414,14 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + if (!isTextboxReceiving || !isKeyboard) + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); // legacy events - if (button.TryGetKeyboard(out Keys key)) + if (isKeyboard) { - if (key != Keys.None) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + if (!isTextboxReceiving && keyboardKey != Keys.None) + ControlEvents.InvokeKeyReleased(this.Monitor, keyboardKey); } else if (button.TryGetController(out Buttons controllerButton)) { -- cgit From 6cf4742bcaef633cb2f42fe2b19f8adaadb50491 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 13:38:09 -0500 Subject: fix some JSON field names being case-sensitive --- docs/release-notes.md | 1 + src/SMAPI/Framework/InternalExtensions.cs | 16 ++++++++++++++++ .../CrossplatformConverters/ColorConverter.cs | 8 ++++---- .../CrossplatformConverters/PointConverter.cs | 4 ++-- .../CrossplatformConverters/RectangleConverter.cs | 10 +++++----- .../SmapiConverters/ManifestDependencyArrayConverter.cs | 6 +++--- .../SmapiConverters/SemanticVersionConverter.cs | 8 ++++---- 7 files changed, 35 insertions(+), 18 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2840798d..f2dde81d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * 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. + * Fixed some JSON field names being case-sensitive. * For SMAPI developers: * Overhauled mod DB format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 71489627..ce67ae18 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -148,5 +149,20 @@ namespace StardewModdingAPI.Framework // get result return reflection.GetField(Game1.spriteBatch, fieldName).GetValue(); } + + /**** + ** Json.NET + ****/ + /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. + /// The value type. + /// The JSON object to search. + /// The field name. + public static T ValueIgnoreCase(this JObject obj, string fieldName) + { + JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); + return token != null + ? token.Value() + : default(T); + } } } diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs index f4a2a26e..f1b2f04f 100644 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs @@ -20,10 +20,10 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters /// The path to the current JSON node. protected override Color ReadObject(JObject obj, string path) { - int r = obj.Value(nameof(Color.R)); - int g = obj.Value(nameof(Color.G)); - int b = obj.Value(nameof(Color.B)); - int a = obj.Value(nameof(Color.A)); + int r = obj.ValueIgnoreCase(nameof(Color.R)); + int g = obj.ValueIgnoreCase(nameof(Color.G)); + int b = obj.ValueIgnoreCase(nameof(Color.B)); + int a = obj.ValueIgnoreCase(nameof(Color.A)); return new Color(r, g, b, a); } diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs index 84c70989..434b7ea5 100644 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs @@ -20,8 +20,8 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters /// The path to the current JSON node. protected override Point ReadObject(JObject obj, string path) { - int x = obj.Value(nameof(Point.X)); - int y = obj.Value(nameof(Point.Y)); + int x = obj.ValueIgnoreCase(nameof(Point.X)); + int y = obj.ValueIgnoreCase(nameof(Point.Y)); return new Point(x, y); } diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs index b89551e3..62bc8637 100644 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters /// The path to the current JSON node. protected override Rectangle ReadObject(JObject obj, string path) { - int x = obj.Value(nameof(Rectangle.X)); - int y = obj.Value(nameof(Rectangle.Y)); - int width = obj.Value(nameof(Rectangle.Width)); - int height = obj.Value(nameof(Rectangle.Height)); + int x = obj.ValueIgnoreCase(nameof(Rectangle.X)); + int y = obj.ValueIgnoreCase(nameof(Rectangle.Y)); + int width = obj.ValueIgnoreCase(nameof(Rectangle.Width)); + int height = obj.ValueIgnoreCase(nameof(Rectangle.Height)); return new Rectangle(x, y, width, height); } @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters if (string.IsNullOrWhiteSpace(str)) return Rectangle.Empty; - var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$"); + var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$", RegexOptions.IgnoreCase); if (!match.Success) throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs index 6352e367..4150d5fb 100644 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs @@ -40,9 +40,9 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters List result = new List(); foreach (JObject obj in JArray.Load(reader).Children()) { - string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); - string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); - bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; + string uniqueID = obj.ValueIgnoreCase(nameof(IManifestDependency.UniqueID)); + string minVersion = obj.ValueIgnoreCase(nameof(IManifestDependency.MinimumVersion)); + bool required = obj.ValueIgnoreCase(nameof(IManifestDependency.IsRequired)) ?? true; result.Add(new ManifestDependency(uniqueID, minVersion, required)); } return result.ToArray(); diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs index 50181809..7ee7e29b 100644 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs @@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters /// The path to the current JSON node. protected override ISemanticVersion ReadObject(JObject obj, string path) { - int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); - string build = obj.Value(nameof(ISemanticVersion.Build)); + int major = obj.ValueIgnoreCase(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); + string build = obj.ValueIgnoreCase(nameof(ISemanticVersion.Build)); return new LegacyManifestVersion(major, minor, patch, build); } -- 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') 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 5060739d62dfce0c72a5d3eeb93332aad7aa929f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 13:58:05 -0500 Subject: update compatibility list --- src/SMAPI/StardewModdingAPI.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index a9d3673c..c7527275 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -603,7 +603,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "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 + "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) }, "Expanded Fridge": { -- 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') 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 049952de33b9d3e1ba81c27212f75268d8ed76f1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 19:42:04 -0500 Subject: simplify content pack list (#436) --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e0064714..825a8401 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -823,7 +823,7 @@ namespace StardewModdingAPI this.Monitor.Log( $" {metadata.DisplayName} {manifest.Version}" + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (metadata.IsContentPack ? $" | content pack for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), LogLevel.Info ); -- cgit From 3b4e81bf69e28c9bcc33c782f58e5099d73c4f91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 20:18:30 -0500 Subject: encapsulate path utilities for reuse, add unit tests --- src/SMAPI.Tests/Core/PathUtilitiesTests.cs | 70 +++++++++++++++++++++++++ src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 1 + src/SMAPI/Framework/Content/ContentCache.cs | 19 ++----- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 3 +- src/SMAPI/Framework/SContentManager.cs | 18 +------ src/SMAPI/Framework/Utilities/PathUtilities.cs | 59 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 138 insertions(+), 33 deletions(-) create mode 100644 src/SMAPI.Tests/Core/PathUtilitiesTests.cs create mode 100644 src/SMAPI/Framework/Utilities/PathUtilities.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs b/src/SMAPI.Tests/Core/PathUtilitiesTests.cs new file mode 100644 index 00000000..268ba504 --- /dev/null +++ b/src/SMAPI.Tests/Core/PathUtilitiesTests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using StardewModdingAPI.Framework.Utilities; + +namespace StardewModdingAPI.Tests.Core +{ + /// Unit tests for . + [TestFixture] + public class PathUtilitiesTests + { + /********* + ** Unit tests + *********/ + [Test(Description = "Assert that GetSegments returns the expected values.")] + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "")] + [TestCase("///", ExpectedResult = "")] + [TestCase("/usr/bin", ExpectedResult = "usr|bin")] + [TestCase("/usr//bin//", ExpectedResult = "usr|bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "usr|bin|..|.|boop.exe")] + [TestCase(@"C:", ExpectedResult = "C:")] + [TestCase(@"C:/boop", ExpectedResult = "C:|boop")] + [TestCase(@"C:\boop\/usr//bin//.././boop.exe", ExpectedResult = "C:|boop|usr|bin|..|.|boop.exe")] + public string GetSegments(string path) + { + return string.Join("|", PathUtilities.GetSegments(path)); + } + + [Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] +#if SMAPI_FOR_WINDOWS + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "")] + [TestCase("///", ExpectedResult = "")] + [TestCase("/usr/bin", ExpectedResult = @"usr\bin")] + [TestCase("/usr//bin//", ExpectedResult = @"usr\bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = @"usr\bin\..\.\boop.exe")] + [TestCase("C:", ExpectedResult = "C:")] + [TestCase("C:/boop", ExpectedResult = @"C:\boop")] + [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = @"C:\usr\bin\..\.\boop.exe")] +#else + [TestCase("", ExpectedResult = "")] + [TestCase("/", ExpectedResult = "/")] + [TestCase("///", ExpectedResult = "/")] + [TestCase("/usr/bin", ExpectedResult = "/usr/bin")] + [TestCase("/usr//bin//", ExpectedResult = "/usr/bin")] + [TestCase("/usr//bin//.././boop.exe", ExpectedResult = "/usr/bin/.././boop.exe")] + [TestCase("C:", ExpectedResult = "C:")] + [TestCase("C:/boop", ExpectedResult = "C:/boop")] + [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] +#endif + public string NormalisePathSeparators(string path) + { + return PathUtilities.NormalisePathSeparators(path); + } + + [Test(Description = "Assert that GetRelativePath returns the expected values.")] +#if SMAPI_FOR_WINDOWS + [TestCase(@"C:\", @"C:\", ExpectedResult = "./")] + [TestCase(@"C:\grandparent\parent\child", @"C:\grandparent\parent\sibling", ExpectedResult = @"..\sibling")] + [TestCase(@"C:\grandparent\parent\child", @"C:\cousin\file.exe", ExpectedResult = @"..\..\..\cousin\file.exe")] +#else + [TestCase("/", "/", ExpectedResult = "./")] + [TestCase("/grandparent/parent/child", "/grandparent/parent/sibling", ExpectedResult = "../sibling")] + [TestCase("/grandparent/parent/child", "/cousin/file.exe", ExpectedResult = "../../../cousin/file.exe")] +#endif + public string GetRelativePath(string sourceDir, string targetPath) + { + return PathUtilities.GetRelativePath(sourceDir, targetPath); + } + } +} diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 6e7fa1f0..e4b2c8c6 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -49,6 +49,7 @@ Properties\GlobalAssemblyInfo.cs + diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 4508e641..533da398 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewValley; namespace StardewModdingAPI.Framework.Content @@ -18,12 +19,6 @@ namespace StardewModdingAPI.Framework.Content /// The underlying asset cache. private readonly IDictionary Cache; - /// The possible directory separator characters in an asset key. - private readonly char[] PossiblePathSeparators; - - /// The preferred directory separator chaeacter in an asset key. - private readonly string PreferredPathSeparator; - /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. private readonly Func NormaliseAssetNameForPlatform; @@ -52,14 +47,10 @@ namespace StardewModdingAPI.Framework.Content /// Construct an instance. /// The underlying content manager whose cache to manage. /// Simplifies access to private game code. - /// The possible directory separator characters in an asset key. - /// The preferred directory separator chaeacter in an asset key. - public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) + public ContentCache(LocalizedContentManager contentManager, Reflector reflection) { // init this.Cache = reflection.GetField>(contentManager, "loadedAssets").GetValue(); - this.PossiblePathSeparators = possiblePathSeparators; - this.PreferredPathSeparator = preferredPathSeparator; // get key normalisation logic if (Constants.TargetPlatform == Platform.Windows) @@ -90,11 +81,7 @@ namespace StardewModdingAPI.Framework.Content [Pure] public string NormalisePathSeparators(string path) { - string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - string normalised = string.Join(this.PreferredPathSeparator, parts); - if (path.StartsWith(this.PreferredPathSeparator)) - normalised = this.PreferredPathSeparator + normalised; // keep root slash - return normalised; + return PathUtilities.NormalisePathSeparators(path); } /// Normalise a cache key so it's consistent with the underlying cache. diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4a1d3853..7d8bec1e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -8,6 +8,7 @@ using System.Linq; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Utilities; using StardewValley; using xTile; using xTile.Format; @@ -238,7 +239,7 @@ namespace StardewModdingAPI.Framework.ModHelpers string imageSource = tilesheet.ImageSource; // validate tilesheet path - if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains("..")) + if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); // get seasonal name (if applicable) diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 463fea0b..fa51bd53 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -35,9 +35,6 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ - /// The preferred directory separator chaeacter in an asset key. - private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); - /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; @@ -75,9 +72,6 @@ namespace StardewModdingAPI.Framework /// Interceptors which edit matching assets after they're loaded. internal IDictionary> Editors { get; } = new Dictionary>(); - /// The possible directory separator characters in an asset key. - internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// The absolute path to the . internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); @@ -100,7 +94,7 @@ namespace StardewModdingAPI.Framework { // init this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); + this.Cache = new ContentCache(this, reflection); this.GetKeyLocale = reflection.GetMethod(this, "languageCode"); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); @@ -399,15 +393,7 @@ namespace StardewModdingAPI.Framework /// The target file path. private string GetRelativePath(string targetPath) { - // convert to URIs - Uri from = new Uri(this.FullRootDirectory + "/"); - Uri to = new Uri(targetPath + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'."); - - // get relative path - return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) - .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath); } /// Get the locale codes (like ja-JP) used in asset keys. diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs new file mode 100644 index 00000000..4fa521f1 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/PathUtilities.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// Provides utilities for normalising file paths. + internal static class PathUtilities + { + /********* + ** Properties + *********/ + /// The possible directory separator characters in a file path. + private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + + /// The preferred directory separator chaeacter in an asset key. + private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); + + + /********* + ** Public methods + *********/ + /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). + /// The path to split. + public static string[] GetSegments(string path) + { + return path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + /// Normalise path separators in a file path. + /// The file path to normalise. + [Pure] + public static string NormalisePathSeparators(string path) + { + string[] parts = PathUtilities.GetSegments(path); + string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); + if (path.StartsWith(PathUtilities.PreferredPathSeparator)) + normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Get a directory or file path relative to a given source path. + /// The source folder path. + /// The target folder or file path. + [Pure] + public static string GetRelativePath(string sourceDir, string targetPath) + { + // convert to URIs + Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); + + // get relative path + return PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 562da60d..14ed1531 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -125,6 +125,7 @@ + -- cgit From c38c2b2c41b97ae7f7e46a4010107221a0e47c91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 20:18:53 -0500 Subject: fix edge case in relative path logic --- src/SMAPI/Framework/Utilities/PathUtilities.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs index 4fa521f1..0233d796 100644 --- a/src/SMAPI/Framework/Utilities/PathUtilities.cs +++ b/src/SMAPI/Framework/Utilities/PathUtilities.cs @@ -53,7 +53,10 @@ namespace StardewModdingAPI.Framework.Utilities throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); // get relative path - return PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + if (relative == "") + relative = "./"; + return relative; } } } -- cgit From b6cc17112d95345de83348dd918ed1f7711926f4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 19 Feb 2018 20:22:01 -0500 Subject: normalise path separators in read/write JSON file methods exposed to mods --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentPack.cs | 3 ++- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index 597fe0e5..f6498f06 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ * 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. * Fixed some JSON field names being case-sensitive. + * Fixed `helper.ReadJsonFile` and `helper.WriteJsonFile` not normalising path separators. * For SMAPI developers: * Overhauled mod DB format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 0a8f223e..071fb872 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -3,6 +3,7 @@ using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; using xTile; namespace StardewModdingAPI.Framework @@ -52,7 +53,7 @@ namespace StardewModdingAPI.Framework /// 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); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFile(path); } diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index c73dc307..07dada7e 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -108,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public TModel ReadJsonFile(string path) where TModel : class { - path = Path.Combine(this.DirectoryPath, path); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFile(path); } @@ -119,7 +120,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public void WriteJsonFile(string path, TModel model) where TModel : class { - path = Path.Combine(this.DirectoryPath, path); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, model); } -- 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') 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 ec1e5a169828d95c6cf0c779089b25627754c894 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 20 Feb 2018 19:43:05 -0500 Subject: support transitional content packs (#436) This commit adds an API to generate a content pack from an arbitrary folder, to support mods which already had their own content pack format before SMAPI standardised it. This lets them support both formats using the same APIs while they transition. --- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 52 ++++++++++++++++++++++++++++- src/SMAPI/IModHelper.cs | 11 ++++++ src/SMAPI/Program.cs | 17 ++++++++-- 3 files changed, 76 insertions(+), 4 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 07dada7e..b5758d21 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Framework.Utilities; @@ -19,6 +20,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The content packs loaded for this mod. private readonly IContentPack[] ContentPacks; + /// Create a transitional content pack. + private readonly Func CreateContentPack; + + /// Manages deprecation warnings. + private readonly DeprecationManager DeprecationManager; + /********* ** Accessors @@ -55,9 +62,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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. + /// Create a transitional content pack. + /// Manages deprecation warnings. /// 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, IEnumerable contentPacks) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -75,6 +84,8 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); this.ContentPacks = contentPacks.ToArray(); + this.CreateContentPack = createContentPack; + this.DeprecationManager = deprecationManager; } /**** @@ -127,6 +138,45 @@ namespace StardewModdingAPI.Framework.ModHelpers /**** ** Content packs ****/ + /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. + /// The absolute directory path containing the content pack files. + /// The content pack's unique ID. + /// The content pack name. + /// The content pack description. + /// The content pack author's name. + /// The content pack version. + [Obsolete("This method supports mods which previously had their own content packs, and shouldn't be used by new mods. It will be removed in SMAPI 3.0.")] + public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) + { + // raise deprecation notice + this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); + + // validate + if(string.IsNullOrWhiteSpace(directoryPath)) + throw new ArgumentNullException(nameof(directoryPath)); + if(string.IsNullOrWhiteSpace(id)) + throw new ArgumentNullException(nameof(id)); + if(string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + if(!Directory.Exists(directoryPath)) + throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); + + // create manifest + IManifest manifest = new Manifest + { + Name = name, + Author = author, + Description = description, + Version = version, + UniqueID = id, + UpdateKeys = new string[0], + ContentPackFor = new ManifestContentPackFor { UniqueID = this.ModID } + }; + + // create content pack + return this.CreateContentPack(directoryPath, manifest); + } + /// Get all content packs loaded for this mod. public IEnumerable GetContentPacks() { diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 96265c85..e9554fdc 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace StardewModdingAPI @@ -60,6 +61,16 @@ namespace StardewModdingAPI /**** ** Content packs ****/ + /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. + /// The absolute directory path containing the content pack files. + /// The content pack's unique ID. + /// The content pack name. + /// The content pack description. + /// The content pack author's name. + /// The content pack version. + [Obsolete("This method supports mods which previously had their own content packs, and shouldn't be used by new mods. It will be removed in SMAPI 3.0.")] + IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version); + /// Get all content packs loaded for this mod. IEnumerable GetContentPacks(); } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2d0908a1..aecf5b30 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -88,6 +88,9 @@ namespace StardewModdingAPI new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper = new JsonHelper(); + /********* ** Public methods @@ -360,14 +363,14 @@ namespace StardewModdingAPI ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), modDatabase).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods - this.LoadMods(mods, new JsonHelper(), this.ContentManager); + this.LoadMods(mods, this.JsonHelper, this.ContentManager); // check for updates this.CheckForUpdatesAsync(mods); @@ -755,7 +758,15 @@ 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, contentPacks); + + IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); + } + + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // get mod instance -- cgit From ddba317142b6b5cbf3efbc867d0b5bd95afcefb2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 22 Feb 2018 20:26:21 -0500 Subject: add friendly warning when an i18n file has duplicate keys due to case-insensitivity (#448) --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index f6498f06..b5d6e32a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * 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 unhelpful error when a translation file has duplicate keys due to case-insensitivity. * Fixed input events being raised for keyboard buttons when a textbox is receiving input. * Fixed some JSON field names being case-sensitive. * Fixed `helper.ReadJsonFile` and `helper.WriteJsonFile` not normalising path separators. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index aecf5b30..e9084b2d 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -765,7 +765,7 @@ namespace StardewModdingAPI IContentHelper packContentHelper = new ContentHelper(contentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } @@ -993,6 +993,24 @@ namespace StardewModdingAPI } } + // validate translations + foreach (string locale in translations.Keys) + { + HashSet keys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet duplicateKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in translations[locale].Keys.ToArray()) + { + if (!keys.Add(key)) + { + duplicateKeys.Add(key); + translations[locale].Remove(key); + } + } + + if (duplicateKeys.Any()) + metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); + } + // update translation TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; translationHelper.SetTranslations(translations); -- cgit From dae5838696ee769f9eab528881ba8ad5da34633b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 22 Feb 2018 20:58:31 -0500 Subject: Revert "suppress keyboard events when a textbox is focused (#445)" This reverts commit 033015066650d4bd67a7df0a7f7addf4c6edf617. --- docs/release-notes.md | 1 - src/SMAPI/Framework/SGame.cs | 22 +++++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index b5d6e32a..8907c8af 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,7 +15,6 @@ * 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 unhelpful error when a translation file has duplicate keys due to case-insensitivity. - * Fixed input events being raised for keyboard buttons when a textbox is receiving input. * Fixed some JSON field names being case-sensitive. * Fixed `helper.ReadJsonFile` and `helper.WriteJsonFile` not normalising path separators. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 73c87118..2d3bad55 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -258,7 +258,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Context: before save creation.", LogLevel.Trace); SaveEvents.InvokeBeforeCreate(this.Monitor); } - + // raise before-save if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { @@ -387,23 +387,20 @@ namespace StardewModdingAPI.Framework } // raise input events - bool isTextboxReceiving = Game1.keyboardDispatcher?.Subscriber?.Selected == true; foreach (var pair in inputState.ActiveButtons) { SButton button = pair.Key; InputStatus status = pair.Value; - bool isKeyboard = button.TryGetKeyboard(out Keys keyboardKey); if (status == InputStatus.Pressed) { - if (!isTextboxReceiving || !isKeyboard) - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); // legacy events - if (isKeyboard) + if (button.TryGetKeyboard(out Keys key)) { - if (!isTextboxReceiving && keyboardKey != Keys.None) - ControlEvents.InvokeKeyPressed(this.Monitor, keyboardKey); + if (key != Keys.None) + ControlEvents.InvokeKeyPressed(this.Monitor, key); } else if (button.TryGetController(out Buttons controllerButton)) { @@ -415,14 +412,13 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - if (!isTextboxReceiving || !isKeyboard) - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); // legacy events - if (isKeyboard) + if (button.TryGetKeyboard(out Keys key)) { - if (!isTextboxReceiving && keyboardKey != Keys.None) - ControlEvents.InvokeKeyReleased(this.Monitor, keyboardKey); + if (key != Keys.None) + ControlEvents.InvokeKeyReleased(this.Monitor, key); } else if (button.TryGetController(out Buttons controllerButton)) { -- cgit From 68528f7decadccb4c5ed62f3fff10aeff22dcd43 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Feb 2018 19:05:23 -0500 Subject: overhaul events to track the mod which added each handler, and log errors under their name (#451) --- docs/release-notes.md | 1 + src/SMAPI/Events/ContentEvents.cs | 28 ++- src/SMAPI/Events/ControlEvents.cs | 123 +++++------- src/SMAPI/Events/GameEvents.cs | 110 +++++------ src/SMAPI/Events/GraphicsEvents.cs | 120 +++++------- src/SMAPI/Events/InputEvents.cs | 46 ++--- src/SMAPI/Events/LocationEvents.cs | 61 +++--- src/SMAPI/Events/MenuEvents.cs | 44 +++-- src/SMAPI/Events/MineEvents.cs | 29 ++- src/SMAPI/Events/PlayerEvents.cs | 45 ++--- src/SMAPI/Events/SaveEvents.cs | 84 ++++----- src/SMAPI/Events/SpecialisedEvents.cs | 25 ++- src/SMAPI/Events/TimeEvents.cs | 41 ++-- src/SMAPI/Framework/Events/EventManager.cs | 249 +++++++++++++++++++++++++ src/SMAPI/Framework/Events/ManagedEvent.cs | 119 ++++++++++++ src/SMAPI/Framework/Events/ManagedEventBase.cs | 81 ++++++++ src/SMAPI/Framework/InternalExtensions.cs | 58 ------ src/SMAPI/Framework/SGame.cs | 142 +++++++------- src/SMAPI/Program.cs | 26 ++- src/SMAPI/StardewModdingAPI.csproj | 3 + 20 files changed, 910 insertions(+), 525 deletions(-) create mode 100644 src/SMAPI/Framework/Events/EventManager.cs create mode 100644 src/SMAPI/Framework/Events/ManagedEvent.cs create mode 100644 src/SMAPI/Framework/Events/ManagedEventBase.cs (limited to 'src/SMAPI') diff --git a/docs/release-notes.md b/docs/release-notes.md index a0f6ef28..08f945e4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ * For players: * **Added support for content packs**. _Content packs are collections of files for a SMAPI mod to load. These can be installed directly under `Mods` like a normal SMAPI mod, get automatic update and compatibility checks, and provide convenient APIs to the mods that read them._ + * Added mod detection to unhandled errors (i.e. most errors will now mention which mod caused them). * Added install scripts for Linux/Mac (no more manual terminal commands!). * Added the required mod's name and URL to dependency errors. * Fixed unhandled mod errors being logged under `[SMAPI]` instead of the mod name. diff --git a/src/SMAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs index 4b4e2ad0..63645258 100644 --- a/src/SMAPI/Events/ContentEvents.cs +++ b/src/SMAPI/Events/ContentEvents.cs @@ -1,29 +1,37 @@ -using System; -using StardewModdingAPI.Framework; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events raised when the game loads content. public static class ContentEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + /********* ** Events *********/ /// Raised after the content language changes. - public static event EventHandler> AfterLocaleChanged; + public static event EventHandler> AfterLocaleChanged + { + add => ContentEvents.EventManager.Content_LocaleChanged.Add(value); + remove => ContentEvents.EventManager.Content_LocaleChanged.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// Raise an event. - /// Encapsulates monitoring and logging. - /// The previous locale. - /// The current locale. - internal static void InvokeAfterLocaleChanged(IMonitor monitor, string oldLocale, string newLocale) + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterLocaleChanged)}", ContentEvents.AfterLocaleChanged?.GetInvocationList(), null, new EventArgsValueChanged(oldLocale, newLocale)); + ContentEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 80d0f547..973bb245 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -1,7 +1,6 @@ -using System; -using Microsoft.Xna.Framework; +using System; using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -9,104 +8,80 @@ namespace StardewModdingAPI.Events public static class ControlEvents { /********* - ** Events + ** Properties *********/ - /// Raised when the changes. That happens when the player presses or releases a key. - public static event EventHandler KeyboardChanged; - - /// Raised when the player presses a keyboard key. - public static event EventHandler KeyPressed; - - /// Raised when the player releases a keyboard key. - public static event EventHandler KeyReleased; - - /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public static event EventHandler MouseChanged; - - /// The player pressed a controller button. This event isn't raised for trigger buttons. - public static event EventHandler ControllerButtonPressed; - - /// The player released a controller button. This event isn't raised for trigger buttons. - public static event EventHandler ControllerButtonReleased; - - /// The player pressed a controller trigger button. - public static event EventHandler ControllerTriggerPressed; - - /// The player released a controller trigger button. - public static event EventHandler ControllerTriggerReleased; + /// The core event manager. + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous keyboard state. - /// The current keyboard state. - internal static void InvokeKeyboardChanged(IMonitor monitor, KeyboardState priorState, KeyboardState newState) + /// Raised when the changes. That happens when the player presses or releases a key. + public static event EventHandler KeyboardChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyboardChanged)}", ControlEvents.KeyboardChanged?.GetInvocationList(), null, new EventArgsKeyboardStateChanged(priorState, newState)); + add => ControlEvents.EventManager.Control_KeyboardChanged.Add(value); + remove => ControlEvents.EventManager.Control_KeyboardChanged.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous mouse state. - /// The current mouse state. - /// The previous mouse position on the screen adjusted for the zoom level. - /// The current mouse position on the screen adjusted for the zoom level. - internal static void InvokeMouseChanged(IMonitor monitor, MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) + /// Raised when the player presses a keyboard key. + public static event EventHandler KeyPressed { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.MouseChanged)}", ControlEvents.MouseChanged?.GetInvocationList(), null, new EventArgsMouseStateChanged(priorState, newState, priorPosition, newPosition)); + add => ControlEvents.EventManager.Control_KeyPressed.Add(value); + remove => ControlEvents.EventManager.Control_KeyPressed.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The keyboard button that was pressed. - internal static void InvokeKeyPressed(IMonitor monitor, Keys key) + /// Raised when the player releases a keyboard key. + public static event EventHandler KeyReleased { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyPressed)}", ControlEvents.KeyPressed?.GetInvocationList(), null, new EventArgsKeyPressed(key)); + add => ControlEvents.EventManager.Control_KeyReleased.Add(value); + remove => ControlEvents.EventManager.Control_KeyReleased.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The keyboard button that was released. - internal static void InvokeKeyReleased(IMonitor monitor, Keys key) + /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. + public static event EventHandler MouseChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyReleased)}", ControlEvents.KeyReleased?.GetInvocationList(), null, new EventArgsKeyPressed(key)); + add => ControlEvents.EventManager.Control_MouseChanged.Add(value); + remove => ControlEvents.EventManager.Control_MouseChanged.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The controller button that was pressed. - internal static void InvokeButtonPressed(IMonitor monitor, Buttons button) + /// The player pressed a controller button. This event isn't raised for trigger buttons. + public static event EventHandler ControllerButtonPressed { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(PlayerIndex.One, button)); + add => ControlEvents.EventManager.Control_ControllerButtonPressed.Add(value); + remove => ControlEvents.EventManager.Control_ControllerButtonPressed.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The controller button that was released. - internal static void InvokeButtonReleased(IMonitor monitor, Buttons button) + /// The player released a controller button. This event isn't raised for trigger buttons. + public static event EventHandler ControllerButtonReleased { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(PlayerIndex.One, button)); + add => ControlEvents.EventManager.Control_ControllerButtonReleased.Add(value); + remove => ControlEvents.EventManager.Control_ControllerButtonReleased.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The trigger button that was pressed. - /// The current trigger value. - internal static void InvokeTriggerPressed(IMonitor monitor, Buttons button, float value) + /// The player pressed a controller trigger button. + public static event EventHandler ControllerTriggerPressed { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(PlayerIndex.One, button, value)); + add => ControlEvents.EventManager.Control_ControllerTriggerPressed.Add(value); + remove => ControlEvents.EventManager.Control_ControllerTriggerPressed.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The trigger button that was pressed. - /// The current trigger value. - internal static void InvokeTriggerReleased(IMonitor monitor, Buttons button, float value) + /// The player released a controller trigger button. + public static event EventHandler ControllerTriggerReleased + { + add => ControlEvents.EventManager.Control_ControllerTriggerReleased.Add(value); + remove => ControlEvents.EventManager.Control_ControllerTriggerReleased.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(PlayerIndex.One, button, value)); + ControlEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs index 3466470d..92879280 100644 --- a/src/SMAPI/Events/GameEvents.cs +++ b/src/SMAPI/Events/GameEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,100 +7,80 @@ namespace StardewModdingAPI.Events public static class GameEvents { /********* - ** Events + ** Properties *********/ - /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . - internal static event EventHandler InitializeInternal; - - /// Raised when the game updates its state (≈60 times per second). - public static event EventHandler UpdateTick; - - /// Raised every other tick (≈30 times per second). - public static event EventHandler SecondUpdateTick; - - /// Raised every fourth tick (≈15 times per second). - public static event EventHandler FourthUpdateTick; - - /// Raised every eighth tick (≈8 times per second). - public static event EventHandler EighthUpdateTick; - - /// Raised every 15th tick (≈4 times per second). - public static event EventHandler QuarterSecondTick; - - /// Raised every 30th tick (≈twice per second). - public static event EventHandler HalfSecondTick; - - /// Raised every 60th tick (≈once per second). - public static event EventHandler OneSecondTick; - - /// Raised once after the game initialises and all methods have been called. - public static event EventHandler FirstUpdateTick; + /// The core event manager. + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeInitialize(IMonitor monitor) + /// Raised when the game updates its state (≈60 times per second). + public static event EventHandler UpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); + add => GameEvents.EventManager.Game_UpdateTick.Add(value); + remove => GameEvents.EventManager.Game_UpdateTick.Remove(value); } - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeUpdateTick(IMonitor monitor) + /// Raised every other tick (≈30 times per second). + public static event EventHandler SecondUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_SecondUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_SecondUpdateTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeSecondUpdateTick(IMonitor monitor) + /// Raised every fourth tick (≈15 times per second). + public static event EventHandler FourthUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_FourthUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_FourthUpdateTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFourthUpdateTick(IMonitor monitor) + /// Raised every eighth tick (≈8 times per second). + public static event EventHandler EighthUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_EighthUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_EighthUpdateTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeEighthUpdateTick(IMonitor monitor) + /// Raised every 15th tick (≈4 times per second). + public static event EventHandler QuarterSecondTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_QuarterSecondTick.Add(value); + remove => GameEvents.EventManager.Game_QuarterSecondTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeQuarterSecondTick(IMonitor monitor) + /// Raised every 30th tick (≈twice per second). + public static event EventHandler HalfSecondTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_HalfSecondTick.Add(value); + remove => GameEvents.EventManager.Game_HalfSecondTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeHalfSecondTick(IMonitor monitor) + /// Raised every 60th tick (≈once per second). + public static event EventHandler OneSecondTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_OneSecondTick.Add(value); + remove => GameEvents.EventManager.Game_OneSecondTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeOneSecondTick(IMonitor monitor) + /// Raised once after the game initialises and all methods have been called. + public static event EventHandler FirstUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_FirstUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_FirstUpdateTick.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFirstUpdateTick(IMonitor monitor) + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList()); + GameEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs index fff51bed..e1ff4ee7 100644 --- a/src/SMAPI/Events/GraphicsEvents.cs +++ b/src/SMAPI/Events/GraphicsEvents.cs @@ -1,116 +1,88 @@ -using System; -using StardewModdingAPI.Framework; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events raised during the game's draw loop, when the game is rendering content to the window. public static class GraphicsEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + /********* ** Events *********/ - /**** - ** Generic events - ****/ /// Raised after the game window is resized. - public static event EventHandler Resize; + public static event EventHandler Resize + { + add => GraphicsEvents.EventManager.Graphics_Resize.Add(value); + remove => GraphicsEvents.EventManager.Graphics_Resize.Remove(value); + } /**** ** Main render events ****/ /// Raised before drawing the world to the screen. - public static event EventHandler OnPreRenderEvent; + public static event EventHandler OnPreRenderEvent + { + add => GraphicsEvents.EventManager.Graphics_OnPreRenderEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPreRenderEvent.Remove(value); + } /// Raised after drawing the world to the screen. - public static event EventHandler OnPostRenderEvent; - - /**** - ** HUD events - ****/ - /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public static event EventHandler OnPreRenderHudEvent; - - /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public static event EventHandler OnPostRenderHudEvent; - - /**** - ** GUI events - ****/ - /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public static event EventHandler OnPreRenderGuiEvent; - - /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public static event EventHandler OnPostRenderGuiEvent; - - - /********* - ** Internal methods - *********/ - /**** - ** Generic events - ****/ - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeResize(IMonitor monitor) + public static event EventHandler OnPostRenderEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPostRenderEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPostRenderEvent.Remove(value); } /**** - ** Main render events + ** HUD events ****/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPreRenderEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderEvent)}", GraphicsEvents.OnPreRenderEvent?.GetInvocationList()); - } - - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPostRenderEvent(IMonitor monitor) + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public static event EventHandler OnPreRenderHudEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderEvent)}", GraphicsEvents.OnPostRenderEvent?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPreRenderHudEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPreRenderHudEvent.Remove(value); } - /// Get whether there are any post-render event listeners. - internal static bool HasPostRenderListeners() + /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public static event EventHandler OnPostRenderHudEvent { - return GraphicsEvents.OnPostRenderEvent != null; + add => GraphicsEvents.EventManager.Graphics_OnPostRenderHudEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPostRenderHudEvent.Remove(value); } /**** ** GUI events ****/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPreRenderGuiEvent(IMonitor monitor) + /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public static event EventHandler OnPreRenderGuiEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderGuiEvent)}", GraphicsEvents.OnPreRenderGuiEvent?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPreRenderGuiEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPreRenderGuiEvent.Remove(value); } - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPostRenderGuiEvent(IMonitor monitor) + /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public static event EventHandler OnPostRenderGuiEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderGuiEvent)}", GraphicsEvents.OnPostRenderGuiEvent?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPostRenderGuiEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPostRenderGuiEvent.Remove(value); } - /**** - ** HUD events - ****/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPreRenderHudEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderHudEvent)}", GraphicsEvents.OnPreRenderHudEvent?.GetInvocationList()); - } - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPostRenderHudEvent(IMonitor monitor) + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderHudEvent)}", GraphicsEvents.OnPostRenderHudEvent?.GetInvocationList()); + GraphicsEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index 985aed99..84d7ce5d 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -1,44 +1,44 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events raised when the player uses a controller, keyboard, or mouse button. public static class InputEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + /********* ** Events *********/ /// Raised when the player presses a button on the keyboard, controller, or mouse. - public static event EventHandler ButtonPressed; + public static event EventHandler ButtonPressed + { + add => InputEvents.EventManager.Input_ButtonPressed.Add(value); + remove => InputEvents.EventManager.Input_ButtonPressed.Remove(value); + } /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. - public static event EventHandler ButtonReleased; + public static event EventHandler ButtonReleased + { + add => InputEvents.EventManager.Input_ButtonReleased.Add(value); + remove => InputEvents.EventManager.Input_ButtonReleased.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// Whether the input should trigger actions on the affected tile. - /// Whether the input should use tools on the affected tile. - internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) - { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// Whether the input should trigger actions on the affected tile. - /// Whether the input should use tools on the affected tile. - internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); + InputEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index b834bc1c..81d13e9f 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework; -using StardewModdingAPI.Framework; -using StardewValley; -using Object = StardewValley.Object; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -11,44 +7,45 @@ namespace StardewModdingAPI.Events public static class LocationEvents { /********* - ** Events + ** Properties *********/ - /// Raised after the player warps to a new location. - public static event EventHandler CurrentLocationChanged; - - /// Raised after a game location is added or removed. - public static event EventHandler LocationsChanged; - - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - public static event EventHandler LocationObjectsChanged; + /// The core event manager. + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The player's previous location. - /// The player's current location. - internal static void InvokeCurrentLocationChanged(IMonitor monitor, GameLocation priorLocation, GameLocation newLocation) + /// Raised after the player warps to a new location. + public static event EventHandler CurrentLocationChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.CurrentLocationChanged)}", LocationEvents.CurrentLocationChanged?.GetInvocationList(), null, new EventArgsCurrentLocationChanged(priorLocation, newLocation)); + add => LocationEvents.EventManager.Location_CurrentLocationChanged.Add(value); + remove => LocationEvents.EventManager.Location_CurrentLocationChanged.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The current list of game locations. - internal static void InvokeLocationsChanged(IMonitor monitor, List newLocations) + /// Raised after a game location is added or removed. + public static event EventHandler LocationsChanged + { + add => LocationEvents.EventManager.Location_LocationsChanged.Add(value); + remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value); + } + + /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + public static event EventHandler LocationObjectsChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationsChanged)}", LocationEvents.LocationsChanged?.GetInvocationList(), null, new EventArgsGameLocationsChanged(newLocations)); + add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); + remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The current list of objects in the current location. - internal static void InvokeOnNewLocationObject(IMonitor monitor, SerializableDictionary newObjects) + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationObjectsChanged)}", LocationEvents.LocationObjectsChanged?.GetInvocationList(), null, new EventArgsLocationObjectsChanged(newObjects)); + LocationEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs index bd8d897e..7fcc3844 100644 --- a/src/SMAPI/Events/MenuEvents.cs +++ b/src/SMAPI/Events/MenuEvents.cs @@ -1,40 +1,44 @@ -using System; -using StardewModdingAPI.Framework; -using StardewValley.Menus; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events raised when a game menu is opened or closed (including internal menus like the title screen). public static class MenuEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + /********* ** Events *********/ /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. - public static event EventHandler MenuChanged; + public static event EventHandler MenuChanged + { + add => MenuEvents.EventManager.Menu_Changed.Add(value); + remove => MenuEvents.EventManager.Menu_Changed.Remove(value); + } /// Raised after a game menu is closed. - public static event EventHandler MenuClosed; + public static event EventHandler MenuClosed + { + add => MenuEvents.EventManager.Menu_Closed.Add(value); + remove => MenuEvents.EventManager.Menu_Closed.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous menu. - /// The current menu. - internal static void InvokeMenuChanged(IMonitor monitor, IClickableMenu priorMenu, IClickableMenu newMenu) - { - monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuChanged)}", MenuEvents.MenuChanged?.GetInvocationList(), null, new EventArgsClickableMenuChanged(priorMenu, newMenu)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The menu that was closed. - internal static void InvokeMenuClosed(IMonitor monitor, IClickableMenu priorMenu) + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuClosed)}", MenuEvents.MenuClosed?.GetInvocationList(), null, new EventArgsClickableMenuClosed(priorMenu)); + MenuEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs index 9cf7edac..5ee4001b 100644 --- a/src/SMAPI/Events/MineEvents.cs +++ b/src/SMAPI/Events/MineEvents.cs @@ -1,28 +1,37 @@ -using System; -using StardewModdingAPI.Framework; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events raised when something happens in the mines. public static class MineEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + /********* ** Events *********/ /// Raised after the player warps to a new level of the mine. - public static event EventHandler MineLevelChanged; + public static event EventHandler MineLevelChanged + { + add => MineEvents.EventManager.Mine_LevelChanged.Add(value); + remove => MineEvents.EventManager.Mine_LevelChanged.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous mine level. - /// The current mine level. - internal static void InvokeMineLevelChanged(IMonitor monitor, int previousMineLevel, int currentMineLevel) + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(MineEvents)}.{nameof(MineEvents.MineLevelChanged)}", MineEvents.MineLevelChanged?.GetInvocationList(), null, new EventArgsMineLevelChanged(previousMineLevel, currentMineLevel)); + MineEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs index 5a9a9d5f..84a7ff63 100644 --- a/src/SMAPI/Events/PlayerEvents.cs +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -1,43 +1,44 @@ using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI.Framework; -using StardewValley; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events raised when the player data changes. public static class PlayerEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + /********* ** Events *********/ /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public static event EventHandler InventoryChanged; + public static event EventHandler InventoryChanged + { + add => PlayerEvents.EventManager.Player_InventoryChanged.Add(value); + remove => PlayerEvents.EventManager.Player_InventoryChanged.Remove(value); + } /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. - public static event EventHandler LeveledUp; + public static event EventHandler LeveledUp + { + add => PlayerEvents.EventManager.Player_LeveledUp.Add(value); + remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// Raise an event. - /// Encapsulates monitoring and logging. - /// The player's inventory. - /// The inventory changes. - internal static void InvokeInventoryChanged(IMonitor monitor, List inventory, IEnumerable changedItems) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); - } - - /// Rase a event. - /// Encapsulates monitoring and logging. - /// The player skill that leveled up. - /// The new skill level. - internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel) + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel)); + PlayerEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs index 99b6c8d2..62184282 100644 --- a/src/SMAPI/Events/SaveEvents.cs +++ b/src/SMAPI/Events/SaveEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,70 +7,66 @@ namespace StardewModdingAPI.Events public static class SaveEvents { /********* - ** Events + ** Properties *********/ - /// Raised before the game creates the save file. - public static event EventHandler BeforeCreate; - - /// Raised after the game finishes creating the save file. - public static event EventHandler AfterCreate; - - /// Raised before the game begins writes data to the save file. - public static event EventHandler BeforeSave; - - /// Raised after the game finishes writing data to the save file. - public static event EventHandler AfterSave; - - /// Raised after the player loads a save slot. - public static event EventHandler AfterLoad; - - /// Raised after the game returns to the title screen. - public static event EventHandler AfterReturnToTitle; + /// The core event manager. + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeBeforeCreate(IMonitor monitor) + /// Raised before the game creates the save file. + public static event EventHandler BeforeCreate + { + add => SaveEvents.EventManager.Save_BeforeCreate.Add(value); + remove => SaveEvents.EventManager.Save_BeforeCreate.Remove(value); + } + + /// Raised after the game finishes creating the save file. + public static event EventHandler AfterCreate { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeCreate)}", SaveEvents.BeforeCreate?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterCreate.Add(value); + remove => SaveEvents.EventManager.Save_AfterCreate.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterCreated(IMonitor monitor) + /// Raised before the game begins writes data to the save file. + public static event EventHandler BeforeSave { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterCreate)}", SaveEvents.AfterCreate?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_BeforeSave.Add(value); + remove => SaveEvents.EventManager.Save_BeforeSave.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeBeforeSave(IMonitor monitor) + /// Raised after the game finishes writing data to the save file. + public static event EventHandler AfterSave { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeSave)}", SaveEvents.BeforeSave?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterSave.Add(value); + remove => SaveEvents.EventManager.Save_AfterSave.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterSave(IMonitor monitor) + /// Raised after the player loads a save slot. + public static event EventHandler AfterLoad { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterSave)}", SaveEvents.AfterSave?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterLoad.Add(value); + remove => SaveEvents.EventManager.Save_AfterLoad.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterLoad(IMonitor monitor) + /// Raised after the game returns to the title screen. + public static event EventHandler AfterReturnToTitle { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterLoad)}", SaveEvents.AfterLoad?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterReturnToTitle.Add(value); + remove => SaveEvents.EventManager.Save_AfterReturnToTitle.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterReturnToTitle(IMonitor monitor) + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterReturnToTitle)}", SaveEvents.AfterReturnToTitle?.GetInvocationList(), null, EventArgs.Empty); + SaveEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs index 2a36e6e4..33ebf3b2 100644 --- a/src/SMAPI/Events/SpecialisedEvents.cs +++ b/src/SMAPI/Events/SpecialisedEvents.cs @@ -1,26 +1,37 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// Events serving specialised edge cases that shouldn't be used by most mod. public static class SpecialisedEvents { + /********* + ** Properties + *********/ + /// The core event manager. + private static EventManager EventManager; + + /********* ** 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; + public static event EventHandler UnvalidatedUpdateTick + { + add => SpecialisedEvents.EventManager.Specialised_UnvalidatedUpdateTick.Add(value); + remove => SpecialisedEvents.EventManager.Specialised_UnvalidatedUpdateTick.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeUnvalidatedUpdateTick(IMonitor monitor) + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(SpecialisedEvents)}.{nameof(SpecialisedEvents.UnvalidatedUpdateTick)}", SpecialisedEvents.UnvalidatedUpdateTick?.GetInvocationList()); + SpecialisedEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/TimeEvents.cs b/src/SMAPI/Events/TimeEvents.cs index 9aea5e04..f769fd08 100644 --- a/src/SMAPI/Events/TimeEvents.cs +++ b/src/SMAPI/Events/TimeEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,31 +7,38 @@ namespace StardewModdingAPI.Events public static class TimeEvents { /********* - ** Events + ** Properties *********/ - /// Raised after the game begins a new day, including when loading a save. - public static event EventHandler AfterDayStarted; + /// The core event manager. + private static EventManager EventManager; - /// Raised after the in-game clock changes. - public static event EventHandler TimeOfDayChanged; /********* - ** Internal methods + ** Events *********/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterDayStarted(IMonitor monitor) + /// Raised after the game begins a new day, including when loading a save. + public static event EventHandler AfterDayStarted + { + add => TimeEvents.EventManager.Time_AfterDayStarted.Add(value); + remove => TimeEvents.EventManager.Time_AfterDayStarted.Remove(value); + } + + /// Raised after the in-game clock changes. + public static event EventHandler TimeOfDayChanged { - monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); + add => TimeEvents.EventManager.Time_TimeOfDayChanged.Add(value); + remove => TimeEvents.EventManager.Time_TimeOfDayChanged.Remove(value); } - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous time in military time format (e.g. 6:00pm is 1800). - /// The current time in military time format (e.g. 6:10pm is 1810). - internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime) + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); + TimeEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs new file mode 100644 index 00000000..d7c89a76 --- /dev/null +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -0,0 +1,249 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Manages SMAPI events. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Private fields are deliberately named to simplify organisation.")] + internal class EventManager + { + /********* + ** Properties + *********/ + /**** + ** ContentEvents + ****/ + /// Raised after the content language changes. + public readonly ManagedEvent> Content_LocaleChanged; + + /**** + ** ControlEvents + ****/ + /// Raised when the changes. That happens when the player presses or releases a key. + public readonly ManagedEvent Control_KeyboardChanged; + + /// Raised when the player presses a keyboard key. + public readonly ManagedEvent Control_KeyPressed; + + /// Raised when the player releases a keyboard key. + public readonly ManagedEvent Control_KeyReleased; + + /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. + public readonly ManagedEvent Control_MouseChanged; + + /// The player pressed a controller button. This event isn't raised for trigger buttons. + public readonly ManagedEvent Control_ControllerButtonPressed; + + /// The player released a controller button. This event isn't raised for trigger buttons. + public readonly ManagedEvent Control_ControllerButtonReleased; + + /// The player pressed a controller trigger button. + public readonly ManagedEvent Control_ControllerTriggerPressed; + + /// The player released a controller trigger button. + public readonly ManagedEvent Control_ControllerTriggerReleased; + + /**** + ** GameEvents + ****/ + /// Raised once after the game initialises and all methods have been called. + public readonly ManagedEvent Game_FirstUpdateTick; + + /// Raised when the game updates its state (≈60 times per second). + public readonly ManagedEvent Game_UpdateTick; + + /// Raised every other tick (≈30 times per second). + public readonly ManagedEvent Game_SecondUpdateTick; + + /// Raised every fourth tick (≈15 times per second). + public readonly ManagedEvent Game_FourthUpdateTick; + + /// Raised every eighth tick (≈8 times per second). + public readonly ManagedEvent Game_EighthUpdateTick; + + /// Raised every 15th tick (≈4 times per second). + public readonly ManagedEvent Game_QuarterSecondTick; + + /// Raised every 30th tick (≈twice per second). + public readonly ManagedEvent Game_HalfSecondTick; + + /// Raised every 60th tick (≈once per second). + public readonly ManagedEvent Game_OneSecondTick; + + /**** + ** GraphicsEvents + ****/ + /// Raised after the game window is resized. + public readonly ManagedEvent Graphics_Resize; + + /// Raised before drawing the world to the screen. + public readonly ManagedEvent Graphics_OnPreRenderEvent; + + /// Raised after drawing the world to the screen. + public readonly ManagedEvent Graphics_OnPostRenderEvent; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public readonly ManagedEvent Graphics_OnPreRenderHudEvent; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public readonly ManagedEvent Graphics_OnPostRenderHudEvent; + + /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public readonly ManagedEvent Graphics_OnPreRenderGuiEvent; + + /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public readonly ManagedEvent Graphics_OnPostRenderGuiEvent; + + /**** + ** InputEvents + ****/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonPressed; + + /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + public readonly ManagedEvent Input_ButtonReleased; + + /**** + ** LocationEvents + ****/ + /// Raised after the player warps to a new location. + public readonly ManagedEvent Location_CurrentLocationChanged; + + /// Raised after a game location is added or removed. + public readonly ManagedEvent Location_LocationsChanged; + + /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + public readonly ManagedEvent Location_LocationObjectsChanged; + + /**** + ** MenuEvents + ****/ + /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. + public readonly ManagedEvent Menu_Changed; + + /// Raised after a game menu is closed. + public readonly ManagedEvent Menu_Closed; + + /**** + ** MineEvents + ****/ + /// Raised after the player warps to a new level of the mine. + public readonly ManagedEvent Mine_LevelChanged; + + /**** + ** PlayerEvents + ****/ + /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). + public readonly ManagedEvent Player_InventoryChanged; + + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public readonly ManagedEvent Player_LeveledUp; + + /**** + ** SaveEvents + ****/ + /// Raised before the game creates the save file. + public readonly ManagedEvent Save_BeforeCreate; + + /// Raised after the game finishes creating the save file. + public readonly ManagedEvent Save_AfterCreate; + + /// Raised before the game begins writes data to the save file. + public readonly ManagedEvent Save_BeforeSave; + + /// Raised after the game finishes writing data to the save file. + public readonly ManagedEvent Save_AfterSave; + + /// Raised after the player loads a save slot. + public readonly ManagedEvent Save_AfterLoad; + + /// Raised after the game returns to the title screen. + public readonly ManagedEvent Save_AfterReturnToTitle; + + /**** + ** SpecialisedEvents + ****/ + /// 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 readonly ManagedEvent Specialised_UnvalidatedUpdateTick; + + /**** + ** TimeEvents + ****/ + /// Raised after the game begins a new day, including when loading a save. + public readonly ManagedEvent Time_AfterDayStarted; + + /// Raised after the in-game clock changes. + public readonly ManagedEvent Time_TimeOfDayChanged; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public EventManager(IMonitor monitor, ModRegistry modRegistry) + { + // create shortcut initialisers + ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + + // init events + this.Content_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); + + this.Control_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Control_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Control_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Control_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Control_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Control_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Control_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Control_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + + this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); + this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); + this.Game_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); + this.Game_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); + this.Game_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); + this.Game_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); + this.Game_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); + this.Game_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); + + this.Graphics_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); + this.Graphics_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); + this.Graphics_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); + this.Graphics_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); + this.Graphics_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); + this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); + this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); + + this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + + this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); + this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + + this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); + this.Menu_Closed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + + this.Mine_LevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); + + this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); + this.Player_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + + this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); + this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); + this.Save_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); + this.Save_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); + this.Save_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); + this.Save_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); + + this.Specialised_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); + + this.Time_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); + this.Time_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs new file mode 100644 index 00000000..e54a4fd3 --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -0,0 +1,119 @@ +using System; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// An event wrapper which intercepts and logs errors in handler code. + /// The event arguments type. + internal class ManagedEvent : ManagedEventBase> + { + /********* + ** Properties + *********/ + /// The underlying event. + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// Add an event handler. + /// The event handler. + public void Add(EventHandler handler) + { + this.Event += handler; + this.AddTracking(handler, this.Event?.GetInvocationList().Cast>()); + } + + /// Remove an event handler. + /// The event handler. + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); + } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + public void Raise(TEventArgs args) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } + + /// An event wrapper which intercepts and logs errors in handler code. + internal class ManagedEvent : ManagedEventBase + { + /********* + ** Properties + *********/ + /// The underlying event. + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// Add an event handler. + /// The event handler. + public void Add(EventHandler handler) + { + this.Event += handler; + this.AddTracking(handler, this.Event?.GetInvocationList().Cast()); + } + + /// Remove an event handler. + /// The event handler. + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast()); + } + + /// Raise the event and notify all handlers. + public void Raise() + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, EventArgs.Empty); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs new file mode 100644 index 00000000..cc4d89ec --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// The base implementation for an event wrapper which intercepts and logs errors in handler code. + internal abstract class ManagedEventBase + { + /********* + ** Properties + *********/ + /// A human-readable name for the event. + private readonly string EventName; + + /// Writes messages to the log. + private readonly IMonitor Monitor; + + /// The mod registry with which to identify mods. + private readonly ModRegistry ModRegistry; + + /// The display names for the mods which added each delegate. + private readonly IDictionary SourceMods = new Dictionary(); + + /// The cached invocation list. + protected TEventHandler[] CachedInvocationList { get; private set; } + + + /********* + ** Public methods + *********/ + /// Get whether anything is listening to the event. + public bool HasListeners() + { + return this.CachedInvocationList?.Length > 0; + } + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + protected ManagedEventBase(string eventName, IMonitor monitor, ModRegistry modRegistry) + { + this.EventName = eventName; + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Track an event handler. + /// The event handler. + /// The updated event invocation list. + protected void AddTracking(TEventHandler handler, IEnumerable invocationList) + { + this.SourceMods[handler] = this.ModRegistry.GetFromStack(); + this.CachedInvocationList = invocationList.ToArray(); + } + + /// Remove tracking for an event handler. + /// The event handler. + /// The updated event invocation list. + protected void RemoveTracking(TEventHandler handler, IEnumerable invocationList) + { + this.SourceMods.Remove(handler); + this.CachedInvocationList = invocationList.ToArray(); + } + + /// Log an exception from an event handler. + /// The event handler instance. + /// The exception that was raised. + protected void LogError(TEventHandler handler, Exception ex) + { + if (this.SourceMods.TryGetValue(handler, out IModMetadata mod)) + mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + } + } +} diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ce67ae18..bff4807c 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; using Newtonsoft.Json.Linq; @@ -15,63 +14,6 @@ namespace StardewModdingAPI.Framework /**** ** IMonitor ****/ - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments (or null to pass ). - public static void SafelyRaisePlainEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender = null, EventArgs args = null) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast()) - { - // handle SMAPI exiting - if (monitor.IsExiting) - { - monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn); - return; - } - - // raise event - try - { - handler.Invoke(sender, args ?? EventArgs.Empty); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// The event argument object type. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments. - public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast>()) - { - try - { - handler.Invoke(sender, args); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - /// Log a message for the player or developer the first time it occurs. /// The monitor through which to log the message. /// The hash of logged messages. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 2d3bad55..5c45edca 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; @@ -35,6 +36,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// Manages SMAPI events for mods. + private readonly EventManager Events; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -117,6 +121,9 @@ namespace StardewModdingAPI.Framework /// The current game instance. private static SGame Instance; + /// A callback to invoke after the game finishes initialising. + private readonly Action OnGameInitialised; + /**** ** Private wrappers ****/ @@ -132,9 +139,9 @@ namespace StardewModdingAPI.Framework set => SGame.Reflection.GetField(typeof(Game1), nameof(_fps)).SetValue(value); } private static Task _newDayTask => SGame.Reflection.GetField(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetField(this, nameof(bgColor)).GetValue(); + private Color bgColor => SGame.Reflection.GetField(this, nameof(this.bgColor)).GetValue(); public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(lightingBlend)).GetValue(); + public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(this.lightingBlend)).GetValue(); private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke(); private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); @@ -158,13 +165,17 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging. /// Simplifies access to private game code. - internal SGame(IMonitor monitor, Reflector reflection) + /// Manages SMAPI events for mods. + /// A callback to invoke after the game finishes initialising. + internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised) { // initialise this.Monitor = monitor; + this.Events = eventManager; this.FirstUpdate = true; SGame.Instance = this; SGame.Reflection = reflection; + this.OnGameInitialised = onGameInitialised; // set XNA option required by Stardew Valley Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -229,7 +240,7 @@ namespace StardewModdingAPI.Framework if (SGame._newDayTask != null) { base.Update(gameTime); - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } @@ -237,7 +248,7 @@ namespace StardewModdingAPI.Framework if (Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } @@ -256,20 +267,20 @@ namespace StardewModdingAPI.Framework { this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - SaveEvents.InvokeBeforeCreate(this.Monitor); + this.Events.Save_BeforeCreate.Raise(); } - + // raise before-save if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); + this.Events.Save_BeforeSave.Raise(); } // suppress non-save events base.Update(gameTime); - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } if (this.IsBetweenCreateEvents) @@ -277,22 +288,22 @@ namespace StardewModdingAPI.Framework // raise after-create this.IsBetweenCreateEvents = false; this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterCreated(this.Monitor); + this.Events.Save_AfterCreate.Raise(); } if (this.IsBetweenSaveEvents) { // raise after-save this.IsBetweenSaveEvents = false; this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + this.Events.Save_AfterSave.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } /********* - ** Game loaded events + ** Notify SMAPI that game is initialised *********/ if (this.FirstUpdate) - GameEvents.InvokeInitialize(this.Monitor); + this.OnGameInitialised(); /********* ** Locale changed events @@ -305,7 +316,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(oldValue.ToString(), newValue.ToString())); + this.PreviousLocale = newValue; } @@ -322,8 +334,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); Context.IsWorldReady = true; - SaveEvents.InvokeAfterLoad(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + this.Events.Save_AfterLoad.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } } @@ -341,7 +353,7 @@ namespace StardewModdingAPI.Framework this.IsExitingToTitle = false; this.CleanupAfterReturnToTitle(); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + this.Events.Save_AfterReturnToTitle.Raise(); } /********* @@ -354,7 +366,7 @@ namespace StardewModdingAPI.Framework if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) { Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); - GraphicsEvents.InvokeResize(this.Monitor); + this.Events.Graphics_Resize.Raise(); this.PreviousWindowSize = size; } @@ -394,47 +406,47 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - ControlEvents.InvokeKeyPressed(this.Monitor, key); + this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right); + this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); else - ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); + this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right); + this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); else - ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); + this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousInput.KeyboardState, inputState.KeyboardState); + this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); if (inputState.MouseState != this.PreviousInput.MouseState) - ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition); + this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); // track state this.PreviousInput = inputState; @@ -461,9 +473,9 @@ namespace StardewModdingAPI.Framework // raise menu events if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); else - MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); + this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu)); // update previous menu // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) @@ -480,46 +492,46 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(this.PreviousGameLocation, Game1.currentLocation)); } // raise location list changed if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { // raise player leveled up a skill if (Game1.player.combatLevel != this.PreviousCombatLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel)); if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel)); if (Game1.player.fishingLevel != this.PreviousFishingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel)); if (Game1.player.foragingLevel != this.PreviousForagingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel)); if (Game1.player.miningLevel != this.PreviousMiningLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel)); if (Game1.player.luckLevel != this.PreviousLuckLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); // raise player inventory changed ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); if (changedItems.Any()) - PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems); + this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.items, changedItems.ToList())); // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects)); // raise time changed if (Game1.timeOfDay != this.PreviousTime) - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.PreviousTime, Game1.timeOfDay)); // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(this.PreviousMineLevel, Game1.mine.mineLevel)); } // update state @@ -553,25 +565,25 @@ namespace StardewModdingAPI.Framework /********* ** Update events *********/ - SpecialisedEvents.InvokeUnvalidatedUpdateTick(this.Monitor); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); if (this.FirstUpdate) { this.FirstUpdate = false; - GameEvents.InvokeFirstUpdateTick(this.Monitor); + this.Events.Game_FirstUpdateTick.Raise(); } - GameEvents.InvokeUpdateTick(this.Monitor); + this.Events.Game_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) - GameEvents.InvokeSecondUpdateTick(this.Monitor); + this.Events.Game_SecondUpdateTick.Raise(); if (this.CurrentUpdateTick % 4 == 0) - GameEvents.InvokeFourthUpdateTick(this.Monitor); + this.Events.Game_FourthUpdateTick.Raise(); if (this.CurrentUpdateTick % 8 == 0) - GameEvents.InvokeEighthUpdateTick(this.Monitor); + this.Events.Game_EighthUpdateTick.Raise(); if (this.CurrentUpdateTick % 15 == 0) - GameEvents.InvokeQuarterSecondTick(this.Monitor); + this.Events.Game_QuarterSecondTick.Raise(); if (this.CurrentUpdateTick % 30 == 0) - GameEvents.InvokeHalfSecondTick(this.Monitor); + this.Events.Game_HalfSecondTick.Raise(); if (this.CurrentUpdateTick % 60 == 0) - GameEvents.InvokeOneSecondTick(this.Monitor); + this.Events.Game_OneSecondTick.Raise(); this.CurrentUpdateTick += 1; if (this.CurrentUpdateTick >= 60) this.CurrentUpdateTick = 0; @@ -680,9 +692,9 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -704,9 +716,9 @@ namespace StardewModdingAPI.Framework try { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -771,9 +783,9 @@ namespace StardewModdingAPI.Framework { try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -858,7 +870,7 @@ namespace StardewModdingAPI.Framework Game1.bloom.BeginDraw(); this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); + this.Events.Graphics_OnPreRenderEvent.Raise(); if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -1129,9 +1141,9 @@ namespace StardewModdingAPI.Framework this.drawBillboard(); if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) { - GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); + this.Events.Graphics_OnPreRenderHudEvent.Raise(); this.drawHUD(); - GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); + this.Events.Graphics_OnPostRenderHudEvent.Raise(); } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); @@ -1263,9 +1275,9 @@ namespace StardewModdingAPI.Framework { try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -1350,11 +1362,11 @@ namespace StardewModdingAPI.Framework /// Whether to create a new sprite batch. private void RaisePostRender(bool needsNewBatch = false) { - if (GraphicsEvents.HasPostRenderListeners()) + if (this.Events.Graphics_OnPostRenderEvent.HasListeners()) { if (needsNewBatch) Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + this.Events.Graphics_OnPostRenderEvent.Raise(); if (needsNewBatch) Game1.spriteBatch.End(); } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e9084b2d..47db8e86 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -17,6 +17,7 @@ using Newtonsoft.Json; using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.ModData; @@ -65,7 +66,7 @@ namespace StardewModdingAPI /// Tracks the installed mods. /// This is initialised after the game starts. - private ModRegistry ModRegistry; + private readonly ModRegistry ModRegistry = new ModRegistry(); /// Manages deprecation warnings. /// This is initialised after the game starts. @@ -75,6 +76,9 @@ namespace StardewModdingAPI /// This is initialised after the game starts. private CommandManager CommandManager; + /// Manages SMAPI events for mods. + private readonly EventManager EventManager; + /// Whether the game is currently running. private bool IsGameRunning; @@ -128,8 +132,24 @@ namespace StardewModdingAPI /// The full file path to which to write log messages. public Program(bool writeToConsole, string logPath) { + // init basics this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; + this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + + // hook up events + ContentEvents.Init(this.EventManager); + ControlEvents.Init(this.EventManager); + GameEvents.Init(this.EventManager); + GraphicsEvents.Init(this.EventManager); + InputEvents.Init(this.EventManager); + LocationEvents.Init(this.EventManager); + MenuEvents.Init(this.EventManager); + MineEvents.Init(this.EventManager); + PlayerEvents.Init(this.EventManager); + SaveEvents.Init(this.EventManager); + SpecialisedEvents.Init(this.EventManager); + TimeEvents.Init(this.EventManager); } /// Launch SMAPI. @@ -170,7 +190,7 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // override game - this.GameInstance = new SGame(this.Monitor, this.Reflection); + this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -198,7 +218,6 @@ namespace StardewModdingAPI ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); - GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles @@ -326,7 +345,6 @@ namespace StardewModdingAPI this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components - this.ModRegistry = new ModRegistry(); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 14ed1531..8ef3022f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,9 +85,12 @@ Properties\GlobalAssemblyInfo.cs + + + -- 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') 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(@"^\[(?