From 3c3953a7fdca6e79f50a4a5474be69ca6aab6446 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 18:18:04 -0400 Subject: add support for minimum dependency versions (#286) --- .../Framework/Serialisation/ManifestFieldConverter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework/Serialisation') diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 6b5a6aaa..7acb5fd0 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -51,7 +51,8 @@ namespace StardewModdingAPI.Framework.Serialisation foreach (JObject obj in JArray.Load(reader).Children()) { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); - result.Add(new ManifestDependency(uniqueID)); + string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); + result.Add(new ManifestDependency(uniqueID, minVersion)); } return result.ToArray(); } -- cgit From b46776a4fbabe765b81751f8c4984cdd8a207419 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 22:08:56 -0400 Subject: enable string versions in manifest.json (#308) --- release-notes.md | 1 + .../Serialisation/ManifestFieldConverter.cs | 25 ++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework/Serialisation') diff --git a/release-notes.md b/release-notes.md index 8a8aa46e..851e6abe 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,7 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...2.0). For mod developers: +* The manifest.json version can now be specified as a string. * Added `ContentEvents.AssetLoading` event with a helper which lets you intercept the XNB content load, and dynamically adjust or replace the content being loaded (including support for patching images). diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 7acb5fd0..7a59f134 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -36,12 +36,25 @@ namespace StardewModdingAPI.Framework.Serialisation // semantic version if (objectType == typeof(ISemanticVersion)) { - JObject obj = JObject.Load(reader); - 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)); - return new SemanticVersion(major, minor, patch, build); + JToken token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.Object: + { + JObject obj = (JObject)token; + 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)); + return new SemanticVersion(major, minor, patch, build); + } + + case JTokenType.String: + return new SemanticVersion(token.Value()); + + default: + throw new FormatException($"Can't parse {token.Type} token as a semantic version, must be an object or string."); + } } // manifest dependency -- cgit From fb8fefea00aacd603e68fbdbaecd27e4c451cc82 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 22:11:48 -0400 Subject: show friendly error when parsing a manifest version fails (#308) --- .../Framework/Exceptions/SParseException.cs | 17 +++++++++++++++++ .../Framework/ModLoading/ModResolver.cs | 5 +++++ .../Framework/Serialisation/ManifestFieldConverter.cs | 12 ++++++++++-- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SParseException.cs (limited to 'src/StardewModdingAPI/Framework/Serialisation') diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs new file mode 100644 index 00000000..f7133ee7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index dc140483..045b175c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; @@ -45,6 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) error = "its manifest doesn't set an entry DLL."; } + catch (SParseException ex) + { + error = $"parsing its manifest failed: {ex.Message}"; + } catch (Exception ex) { error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 7a59f134..e6d62d50 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.Serialisation @@ -50,10 +51,17 @@ namespace StardewModdingAPI.Framework.Serialisation } case JTokenType.String: - return new SemanticVersion(token.Value()); + { + string str = token.Value(); + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta."); + return version; + } default: - throw new FormatException($"Can't parse {token.Type} token as a semantic version, must be an object or string."); + throw new SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 465a5ea7..77d3b12b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -151,6 +151,7 @@ + -- cgit From e69d1615c4ff1cf93e51f83b66f7d32fe6baf942 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Jul 2017 19:32:40 -0400 Subject: throw more useful error when JSON file is invalid (#314) --- release-notes.md | 1 + .../Framework/Serialisation/JsonHelper.cs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework/Serialisation') diff --git a/release-notes.md b/release-notes.md index 4b4a3447..ae2f853d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -21,6 +21,7 @@ For players: * `list_items` now shows all items in the game. You can search by item type like `list_items weapon`, or search by item name like `list_items galaxy sword`. * `list_items` now also matches translated item names when playing in another language. * `list_item_types` is a new command to see a list of item types. +* Added clearer error when a `config.json` is invalid. For modders: * You can now specify minimum dependency versions in `manifest.json`. diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs index 64d8738e..6431394c 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs @@ -51,7 +51,21 @@ namespace StardewModdingAPI.Framework.Serialisation } // deserialise model - return JsonConvert.DeserializeObject(json, this.JsonSettings); + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException ex) + { + string message = $"The file at {fullPath} doesn't seem to be valid JSON."; + + string text = File.ReadAllText(fullPath); + if (text.Contains("“") || text.Contains("”")) + message += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + + message += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(message); + } } /// Save to a JSON file. -- cgit From e2b9a4bab3e078851a289ad0a19b555dde09308e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Jul 2017 15:17:47 -0400 Subject: serialise SButtons as string in config.json (#316) --- src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework/Serialisation') diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs index 6431394c..3193aa3c 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; +using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.Serialisation { @@ -19,7 +20,7 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys)) + new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) } }; -- cgit From d928bf188e9ab171223bc07d7209d2887d954642 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Jul 2017 17:46:04 -0400 Subject: add optional mod dependencies in SMAPI 2.0 (#287) --- release-notes.md | 4 ++- .../Core/ModResolverTests.cs | 34 ++++++++++++++++++++++ .../Framework/ModLoading/ModResolver.cs | 25 ++++++++++++---- .../Framework/Models/ManifestDependency.cs | 14 ++++++++- .../Serialisation/ManifestFieldConverter.cs | 5 ++++ src/StardewModdingAPI/IManifestDependency.cs | 5 ++++ 6 files changed, 80 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Framework/Serialisation') diff --git a/release-notes.md b/release-notes.md index dcc21aaf..ca0e00f7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,7 +11,9 @@ For mod developers: * Added `InputEvents` which unify keyboard, mouse, and controller input for much simpler input handling (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Input_events)). * Added useful `InputEvents` metadata like the cursor position, grab tile, etc. * Added ability to prevent the game from handling a button press via `InputEvents`. -* The `manifest.json` version can now be specified as a string. +* In `manifest.json`: + * Dependencies can now be optional. + * The version can now be a string like `"1.0-alpha"` instead of a structure. * Removed all deprecated code. ## 1.15 diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 36cc3495..b451465e 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -411,6 +411,40 @@ namespace StardewModdingAPI.Tests.Core Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); } +#if SMAPI_2_0 + [Test(Description = "Assert that optional dependencies are sorted correctly if present.")] + public void ProcessDependencies_IfOptional() + { + // arrange + // A ◀── B + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + 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(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + } + + [Test(Description = "Assert that optional dependencies are accepted if they're missing.")] + public void ProcessDependencies_IfOptional_SucceedsIfMissing() + { + // arrange + // A ◀── B where A doesn't exist + 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(); + + // assert + Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod."); + } +#endif + /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 9c56aaa4..38dddce7 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -228,13 +228,24 @@ namespace StardewModdingAPI.Framework.ModLoading 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 } + select new + { + ID = entry.UniqueID, + MinVersion = entry.MinimumVersion, + Mod = dependencyMod, + IsRequired = +#if SMAPI_2_0 + entry.IsRequired +#else + true +#endif + } ) .ToArray(); // missing required dependencies, mark failed { - string[] failedIDs = (from entry in dependencies where entry.Mod == null select entry.ID).ToArray(); + string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); if (failedIDs.Any()) { sortedMods.Push(mod); @@ -248,7 +259,7 @@ namespace StardewModdingAPI.Framework.ModLoading string[] failedLabels = ( from entry in dependencies - where entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) + where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" ) .ToArray(); @@ -265,11 +276,15 @@ namespace StardewModdingAPI.Framework.ModLoading states[mod] = ModDependencyStatus.Checking; // recursively sort dependencies - IModMetadata[] modsToLoadFirst = dependencies.Select(p => p.Mod).ToArray(); - foreach (IModMetadata requiredMod in modsToLoadFirst) + foreach (var dependency in dependencies) { + IModMetadata requiredMod = dependency.Mod; var subchain = new List(currentChain) { mod }; + // ignore missing optional dependency + if (!dependency.IsRequired && requiredMod == null) + continue; + // detect dependency loop if (states[requiredMod] == ModDependencyStatus.Checking) { diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs index a0ff0c90..25d92a29 100644 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs @@ -12,6 +12,10 @@ /// The minimum required version (if any). public ISemanticVersion MinimumVersion { get; set; } +#if SMAPI_2_0 + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } +#endif /********* ** Public methods @@ -19,12 +23,20 @@ /// Construct an instance. /// The unique mod ID to require. /// The minimum required version (if any). - public ManifestDependency(string uniqueID, string minimumVersion) + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion +#if SMAPI_2_0 + , bool required = true +#endif + ) { this.UniqueID = uniqueID; this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) ? new SemanticVersion(minimumVersion) : null; +#if SMAPI_2_0 + this.IsRequired = required; +#endif } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index e6d62d50..5be0f0b6 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -73,7 +73,12 @@ namespace StardewModdingAPI.Framework.Serialisation { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); +#if SMAPI_2_0 + bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); +#else result.Add(new ManifestDependency(uniqueID, minVersion)); +#endif } return result.ToArray(); } diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs index ebb1140e..027c1d59 100644 --- a/src/StardewModdingAPI/IManifestDependency.cs +++ b/src/StardewModdingAPI/IManifestDependency.cs @@ -11,5 +11,10 @@ /// The minimum required version (if any). ISemanticVersion MinimumVersion { get; } + +#if SMAPI_2_0 + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } +#endif } } -- cgit