From 2a7bcb28f6a390e165c2cb8817f333aad750a032 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:13:28 -0400 Subject: add empty toolkit project & fix misleading build configuration name (#532) --- .../Properties/AssemblyInfo.cs | 6 ++++++ .../StardewModdingAPI.Toolkit.csproj | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..20118fce --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("SMAPI.Toolkit")] +[assembly: AssemblyDescription("")] +[assembly: InternalsVisibleTo("StardewModdingAPI")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj new file mode 100644 index 00000000..ffda899a --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -0,0 +1,22 @@ + + + + net4.5;netstandard2.0 + false + + + + ..\..\bin\$(Configuration)\SMAPI.Toolkit + + + + + + + + + + + + + -- cgit From 3129f67eb1f162e06d96854f319b10ccf583f0aa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:14:40 -0400 Subject: add semantic version to toolkit (#532) --- src/SMAPI.Installer/InteractiveInstaller.cs | 12 +- src/SMAPI.Internal/SMAPI.Internal.projitems | 1 - src/SMAPI.Internal/SemanticVersionImpl.cs | 199 ----------------- .../Framework/ModFileManager.cs | 6 +- .../StardewModdingAPI.ModBuildConfig.csproj | 6 + src/SMAPI.Web/Controllers/IndexController.cs | 8 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 4 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 4 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 3 + src/SMAPI/SemanticVersion.cs | 16 +- src/StardewModdingAPI.Toolkit/ISemanticVersion.cs | 46 ++++ .../Properties/AssemblyInfo.cs | 1 + src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 239 +++++++++++++++++++++ 13 files changed, 325 insertions(+), 220 deletions(-) delete mode 100644 src/SMAPI.Internal/SemanticVersionImpl.cs create mode 100644 src/StardewModdingAPI.Toolkit/ISemanticVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit/SemanticVersion.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index ace560e5..6e4cb95d 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -152,7 +152,7 @@ namespace StardewModdingApi.Installer ** Get platform & set window title ****/ Platform platform = EnvironmentUtility.DetectPlatform(); - Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; + Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.WriteLine(); #if SMAPI_FOR_WINDOWS @@ -421,6 +421,16 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ + /// Get the display text for an assembly version. + /// The assembly version. + private string GetDisplayVersion(Version version) + { + string str = $"{version.Major}.{version.Minor}"; + if (version.Build != 0) + str += $".{version.Build}"; + return str; + } + /// Get the value of a key in the Windows registry. /// The full path of the registry key relative to HKLM. /// The name of the value. diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index dadae4b0..33b8cbfa 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -16,6 +16,5 @@ - \ No newline at end of file diff --git a/src/SMAPI.Internal/SemanticVersionImpl.cs b/src/SMAPI.Internal/SemanticVersionImpl.cs deleted file mode 100644 index 7ae34f07..00000000 --- a/src/SMAPI.Internal/SemanticVersionImpl.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI.Internal -{ - /// A low-level implementation of a semantic version with an optional release tag. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). - internal class SemanticVersionImpl : IComparable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - public int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } - - /// An optional prerelease tag. - public string Tag { get; } - - /// A regex pattern matching a version within a larger string. - internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; - - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional prerelease tag. - public SemanticVersionImpl(int major, int minor, int patch, string tag = null) - { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); - } - - /// Construct an instance. - /// The assembly version. - /// The is null. - public SemanticVersionImpl(Version version) - { - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version can't be null."); - - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; - } - - /// Construct an instance. - /// The semantic version string. - /// The is null. - /// The is not a valid semantic version. - public SemanticVersionImpl(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersionImpl.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. - public int CompareTo(SemanticVersionImpl other) - { - if (other == null) - throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); - } - - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The major version to compare with this instance. - /// The minor version to compare with this instance. - /// The patch version to compare with this instance. - /// The prerelease tag to compare with this instance. - public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) - { - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); - bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); - string[] otherParts = otherTag.Split('.', '-'); - for (int i = 0; i < curParts.Length; i++) - { - // longer prerelease tag supercedes if otherwise equal - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - /// Get a string representation of the version. - public override string ToString() - { - // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; - - // tag - string tag = this.Tag; - if (tag != null) - result += $"-{tag}"; - return result; - } - - /// Parse a version string without throwing an exception if it fails. - /// The version string. - /// The parsed representation. - /// Returns whether parsing the version succeeded. - internal static bool TryParse(string version, out SemanticVersionImpl parsed) - { - try - { - parsed = new SemanticVersionImpl(version); - return true; - } - catch - { - parsed = null; - return false; - } - } - - - /********* - ** Private methods - *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) - { - tag = tag?.Trim(); - return !string.IsNullOrWhiteSpace(tag) ? tag : null; - } - } -} diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 3fec8215..41e0201d 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Script.Serialization; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.ModBuildConfig.Framework { @@ -132,9 +132,9 @@ namespace StardewModdingAPI.ModBuildConfig.Framework int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; - return new SemanticVersionImpl(major, minor, patch, tag).ToString(); + return new SemanticVersion(major, minor, patch, tag).ToString(); } - return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ + return new SemanticVersion(versionObj.ToString()).ToString(); // SMAPI 2.0+ } diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index e0ea876f..6a52daac 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -55,6 +55,12 @@ + + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + + diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 08b7363a..0cc3c37a 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Web.Controllers foreach (GitAsset asset in release.Assets) { Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); - if (!match.Success || !SemanticVersionImpl.TryParse(match.Groups["version"].Value, out SemanticVersionImpl version)) + if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; bool isBeta = version.Tag != null; bool isForDevs = match.Groups["forDevs"].Success; @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Web.Controllers public GitAsset Asset { get; } /// The SMAPI version. - public SemanticVersionImpl Version { get; } + public ISemanticVersion Version { get; } /// Whether this is a beta download. public bool IsBeta { get; } @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Web.Controllers /// The SMAPI version. /// Whether this is a beta download. /// Whether this is a 'for developers' download. - public ReleaseVersion(GitRelease release, GitAsset asset, SemanticVersionImpl version, bool isBeta, bool isForDevs) + public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isBeta, bool isForDevs) { this.Release = release; this.Asset = asset; diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 163176fd..c50e643a 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing /// A regex pattern matching an entry in SMAPI's mod list. /// The author name and description are optional. - private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersionImpl.UnboundedVersionPattern + @")(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's content pack list. private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index 1502f5d8..2d6ec603 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Routing.Constraints; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework { @@ -11,6 +11,6 @@ namespace StardewModdingAPI.Web.Framework *********/ /// Construct an instance. public VersionConstraint() - : base(SemanticVersionImpl.Regex) { } + : base(SemanticVersion.Regex) { } } } diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index e4678269..202a8376 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -23,5 +23,8 @@ + + + diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 9db1cf14..3ee3ccf3 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,5 @@ using System; using Newtonsoft.Json; -using StardewModdingAPI.Internal; namespace StardewModdingAPI { @@ -11,7 +10,7 @@ namespace StardewModdingAPI ** Properties *********/ /// The underlying semantic version implementation. - private readonly SemanticVersionImpl Version; + private readonly Toolkit.ISemanticVersion Version; /********* @@ -40,20 +39,20 @@ namespace StardewModdingAPI /// An optional build tag. [JsonConstructor] public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - : this(new SemanticVersionImpl(majorVersion, minorVersion, patchVersion, build)) { } + : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, build)) { } /// Construct an instance. /// The semantic version string. /// The is null. /// The is not a valid semantic version. public SemanticVersion(string version) - : this(new SemanticVersionImpl(version)) { } + : this(new Toolkit.SemanticVersion(version)) { } /// Construct an instance. /// The assembly version. /// The is null. public SemanticVersion(Version version) - : this(new SemanticVersionImpl(version)) { } + : this(new Toolkit.SemanticVersion(version)) { } /// Whether this is a pre-release version. public bool IsPrerelease() @@ -67,7 +66,8 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - return this.Version.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); + Toolkit.ISemanticVersion toolkitOther = new Toolkit.SemanticVersion(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); + return this.Version.CompareTo(toolkitOther); } /// Get whether this version is older than the specified version. @@ -137,7 +137,7 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. internal static bool TryParse(string version, out ISemanticVersion parsed) { - if (SemanticVersionImpl.TryParse(version, out SemanticVersionImpl versionImpl)) + if (Toolkit.SemanticVersion.TryParse(version, out Toolkit.ISemanticVersion versionImpl)) { parsed = new SemanticVersion(versionImpl); return true; @@ -153,7 +153,7 @@ namespace StardewModdingAPI *********/ /// Construct an instance. /// The underlying semantic version implementation. - private SemanticVersion(SemanticVersionImpl version) + private SemanticVersion(Toolkit.ISemanticVersion version) { this.Version = version; } diff --git a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs new file mode 100644 index 00000000..ca62d393 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs @@ -0,0 +1,46 @@ +using System; + +namespace StardewModdingAPI.Toolkit +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + int Patch { get; } + + /// An optional prerelease tag. + string Tag { get; } + + + /********* + ** Accessors + *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 20118fce..9b55dac6 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("")] [assembly: InternalsVisibleTo("StardewModdingAPI")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs new file mode 100644 index 00000000..bd85f990 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -0,0 +1,239 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Toolkit +{ + /// A semantic version with an optional release tag. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + public class SemanticVersion : ISemanticVersion + { + /********* + ** Properties + *********/ + /// A regex pattern matching a version within a larger string. + internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; + + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + public int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + public int Patch { get; } + + /// An optional prerelease tag. + public string Tag { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible fixes. + /// An optional prerelease tag. + public SemanticVersion(int major, int minor, int patch, string tag = null) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.Tag = this.GetNormalisedTag(tag); + } + + /// Construct an instance. + /// The assembly version. + /// The is null. + public SemanticVersion(Version version) + { + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version can't be null."); + + this.Major = version.Major; + this.Minor = version.Minor; + this.Patch = version.Build; + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersion(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersion.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.Major = int.Parse(match.Groups["major"].Value); + this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + public int CompareTo(ISemanticVersion other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(ISemanticVersion other) + { + return other != null && this.CompareTo(other) == 0; + } + + /// Whether this is a pre-release version. + public bool IsPrerelease() + { + return !string.IsNullOrWhiteSpace(this.Tag); + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + public bool IsOlderThan(ISemanticVersion other) + { + return this.CompareTo(other) < 0; + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + public bool IsNewerThan(ISemanticVersion other) + { + return this.CompareTo(other) > 0; + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + public bool IsBetween(ISemanticVersion min, ISemanticVersion max) + { + return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + } + + /// Get a string representation of the version. + public override string ToString() + { + // version + string result = this.Patch != 0 + ? $"{this.Major}.{this.Minor}.{this.Patch}" + : $"{this.Major}.{this.Minor}"; + + // tag + string tag = this.Tag; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + public static bool TryParse(string version, out ISemanticVersion parsed) + { + try + { + parsed = new SemanticVersion(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + return !string.IsNullOrWhiteSpace(tag) ? tag : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The major version to compare with this instance. + /// The minor version to compare with this instance. + /// The patch version to compare with this instance. + /// The prerelease tag to compare with this instance. + private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.Major != otherMajor) + return this.Major.CompareTo(otherMajor); + if (this.Minor != otherMinor) + return this.Minor.CompareTo(otherMinor); + if (this.Patch != otherPatch) + return this.Patch.CompareTo(otherPatch); + if (this.Tag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Tag.Split('.', '-'); + string[] otherParts = otherTag.Split('.', '-'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + } +} -- cgit From 33760fa56bf2defd1b1e5a473cc45111b0b6f3f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:51:04 -0400 Subject: fix post-build step in toolkit project (#532) --- build/common.targets | 10 +++++----- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/build/common.targets b/build/common.targets index 2ee24e52..099d7cc9 100644 --- a/build/common.targets +++ b/build/common.targets @@ -95,7 +95,7 @@ - + @@ -106,15 +106,15 @@ - - - - + + + + diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index ffda899a..1028e657 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -17,6 +17,6 @@ - + -- cgit From 69b17f1db87d9aeb5dd6d6f9c81ac9ac62f2a6d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 02:06:28 -0400 Subject: move PathUtilities into toolkit (#532) --- src/SMAPI.Tests/Core/PathUtilitiesTests.cs | 70 ---------------------- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 8 ++- src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs | 70 ++++++++++++++++++++++ src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ContentCoordinator.cs | 3 +- src/SMAPI/Framework/ContentPack.cs | 2 +- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 2 +- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 10 ++-- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/Utilities/PathUtilities.cs | 65 -------------------- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Utilities/PathUtilities.cs | 65 ++++++++++++++++++++ 13 files changed, 152 insertions(+), 150 deletions(-) delete mode 100644 src/SMAPI.Tests/Core/PathUtilitiesTests.cs create mode 100644 src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs delete mode 100644 src/SMAPI/Framework/Utilities/PathUtilities.cs create mode 100644 src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs b/src/SMAPI.Tests/Core/PathUtilitiesTests.cs deleted file mode 100644 index 268ba504..00000000 --- a/src/SMAPI.Tests/Core/PathUtilitiesTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -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 f4d7b3e3..04c8d12f 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -56,7 +56,7 @@ Properties\GlobalAssemblyInfo.cs - + @@ -73,6 +73,10 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + diff --git a/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs new file mode 100644 index 00000000..229b9a14 --- /dev/null +++ b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Tests.Toolkit +{ + /// 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/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index c2818cdd..a5dfac9d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -4,8 +4,8 @@ using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; namespace StardewModdingAPI.Framework.Content diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index c2614001..1a57dd22 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -9,10 +9,9 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; -using xTile; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 071fb872..ee6df1ec 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -3,7 +3,7 @@ using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; using xTile; namespace StardewModdingAPI.Framework diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index ce26c980..671dc21e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -9,7 +9,7 @@ using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using xTile; using xTile.Format; diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 26fe7198..61d2075c 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -157,13 +157,13 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate - if(string.IsNullOrWhiteSpace(directoryPath)) + if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException(nameof(directoryPath)); - if(string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - if(string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - if(!Directory.Exists(directoryPath)) + if (!Directory.Exists(directoryPath)) throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index b5339183..d46caa55 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -7,7 +7,7 @@ using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs deleted file mode 100644 index 51d45ebd..00000000 --- a/src/SMAPI/Framework/Utilities/PathUtilities.cs +++ /dev/null @@ -1,65 +0,0 @@ -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. - /// The number of segments to match. Any additional segments will be merged into the last returned part. - public static string[] GetSegments(string path, int? limit = null) - { - return limit.HasValue - ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) - : 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 - string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); - if (relative == "") - relative = "./"; - return relative; - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index aeb9b9fb..a1cfcf0c 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -25,9 +25,9 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 926bcbbc..3e6fdb24 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -157,7 +157,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs new file mode 100644 index 00000000..2e74e7d9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides utilities for normalising file paths. + public 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. + /// The number of segments to match. Any additional segments will be merged into the last returned part. + public static string[] GetSegments(string path, int? limit = null) + { + return limit.HasValue + ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) + : 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 + string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + if (relative == "") + relative = "./"; + return relative; + } + } +} -- cgit From 9b87d338aeb432d4417985f4e76b72499ed2ecac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 02:08:30 -0400 Subject: use separate pre-release version number for toolkit (#532) --- src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs | 5 ++++- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 4 ---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 9b55dac6..22dcdd96 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -2,6 +2,9 @@ using System.Reflection; using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] +[assembly: AssemblyProduct("SMAPI Toolkit")] +[assembly: AssemblyVersion("0.1.0")] +[assembly: AssemblyFileVersion("0.1.0")] [assembly: InternalsVisibleTo("StardewModdingAPI")] [assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 1028e657..6859d825 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -9,10 +9,6 @@ ..\..\bin\$(Configuration)\SMAPI.Toolkit - - - - -- cgit From 03860cca868aceb6af67d9a6ad171c8ceb9be7eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 19:26:09 -0400 Subject: add wiki compatibility list parsing to toolkit (#532) --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 2 +- .../Clients/Wiki/WikiCompatibilityClient.cs | 142 +++++++++++++++++++++ .../Clients/Wiki/WikiCompatibilityEntry.cs | 24 ++++ .../Clients/Wiki/WikiCompatibilityStatus.cs | 24 ++++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 33 +++++ .../StardewModdingAPI.Toolkit.csproj | 2 + 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/ModToolkit.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 202a8376..9210565a 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs new file mode 100644 index 00000000..be03a120 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// An HTTP client for fetching mod metadata from the wiki compatibility list. + public class WikiCompatibilityClient : IDisposable + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the wiki API. + /// The base URL for the wiki API. + public WikiCompatibilityClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Fetch mod compatibility entries. + public async Task FetchAsync() + { + // fetch HTML + ResponseModel response = await this.Client + .GetAsync("") + .WithArguments(new + { + action = "parse", + page = "Modding:SMAPI_compatibility", + format = "json" + }) + .As(); + string html = response.Parse.Text["*"]; + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // find mod entries + HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); + if (modNodes == null) + throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); + + // parse + return this.ParseEntries(modNodes).ToArray(); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Parse valid mod compatibility entries. + /// The HTML compatibility entries. + private IEnumerable ParseEntries(IEnumerable nodes) + { + foreach (HtmlNode node in nodes) + { + // parse status + WikiCompatibilityStatus status; + { + string rawStatus = node.GetAttributeValue("data-status", null); + if (rawStatus == null) + continue; // not a mod node? + if (!Enum.TryParse(rawStatus, true, out status)) + throw new InvalidOperationException($"Unknown status '{rawStatus}' when parsing compatibility list."); + } + + // parse unofficial version + ISemanticVersion unofficialVersion = null; + { + string rawUnofficialVersion = node.GetAttributeValue("data-unofficial-version", null); + SemanticVersion.TryParse(rawUnofficialVersion, out unofficialVersion); + } + + // parse other fields + int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); + int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); + string githubRepo = node.GetAttributeValue("data-github", null); + string customSourceUrl = node.GetAttributeValue("data-custom-source", null); + + // yield model + yield return new WikiCompatibilityEntry + { + Status = status, + NexusID = nexusID, + ChucklefishID = chucklefishID, + GitHubRepo = githubRepo, + CustomSourceUrl = customSourceUrl, + UnofficialVersion = unofficialVersion + }; + } + } + + /// Get a nullable integer attribute value. + /// The HTML node. + /// The attribute name. + private int? GetNullableIntAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + if (raw != null && int.TryParse(raw, out int value)) + return value; + return null; + } + + /// The response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseModel + { + /// The parse API results. + public ResponseParseModel Parse { get; set; } + } + + /// The inner response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseParseModel + { + /// The parsed text. + public IDictionary Text { get; set; } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs new file mode 100644 index 00000000..ceeb2de5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// An entry in the mod compatibility list. + public class WikiCompatibilityEntry + { + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The version of the latest unofficial update, if applicable. + public ISemanticVersion UnofficialVersion { get; set; } + + /// The compatibility status. + public WikiCompatibilityStatus Status { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs new file mode 100644 index 00000000..06252b2f --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// The compatibility status for a mod. + public enum WikiCompatibilityStatus + { + /// The mod is compatible. + Ok = 0, + + /// The mod is compatible if you use an optional official download. + Optional = 1, + + /// The mod isn't compatible, but the player can fix it or there's a good alternative. + Workaround = 2, + + /// The mod isn't compatible. + Broken = 3, + + /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. + Abandoned = 4, + + /// The mod is no longer needed and should be removed. + Obsolete = 5 + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs new file mode 100644 index 00000000..6136186e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Toolkit +{ + /// A convenience wrapper for the various tools. + public class ModToolkit + { + /********* + ** Properties + *********/ + /// The default HTTP user agent for the toolkit. + private readonly string UserAgent; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModToolkit() + { + ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version); + this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; + } + + /// Extract mod metadata from the wiki compatibility list. + public async Task GetWikiCompatibilityListAsync() + { + var client = new WikiCompatibilityClient(this.UserAgent); + return await client.FetchAsync(); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 6859d825..6688a2a1 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -10,7 +10,9 @@ + + -- cgit From 738c0ce386590f55c185297065aeda62b1f5d06f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 May 2018 15:21:07 -0400 Subject: improve wiki parsing (#532) --- .../Clients/Wiki/WikiCompatibilityClient.cs | 23 +++++++++++++++++++--- .../Clients/Wiki/WikiCompatibilityEntry.cs | 9 +++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index be03a120..a68a0b4e 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -91,19 +91,25 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } // parse other fields + string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim(); + string[] ids = this.GetAttribute(node, "data-id")?.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); - string githubRepo = node.GetAttributeValue("data-github", null); - string customSourceUrl = node.GetAttributeValue("data-custom-source", null); + string githubRepo = this.GetAttribute(node, "data-github"); + string customSourceUrl = this.GetAttribute(node, "data-custom-source"); + string customUrl = this.GetAttribute(node, "data-custom-url"); // yield model yield return new WikiCompatibilityEntry { + ID = ids, + Name = name, Status = status, NexusID = nexusID, ChucklefishID = chucklefishID, GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, + CustomUrl = customUrl, UnofficialVersion = unofficialVersion }; } @@ -114,12 +120,23 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The attribute name. private int? GetNullableIntAttribute(HtmlNode node, string attributeName) { - string raw = node.GetAttributeValue(attributeName, null); + string raw = this.GetAttribute(node, attributeName); if (raw != null && int.TryParse(raw, out int value)) return value; return null; } + /// Get a strings attribute value. + /// The HTML node. + /// The attribute name. + private string GetAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + if (raw != null) + raw = HtmlEntity.DeEntitize(raw); + return raw; + } + /// The response model for the MediaWiki parse API. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs index ceeb2de5..9aabdcba 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -3,6 +3,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// An entry in the mod compatibility list. public class WikiCompatibilityEntry { + /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + public string[] ID { get; set; } + + /// The mod's display name. + public string Name { get; set; } + /// The mod ID on Nexus. public int? NexusID { get; set; } @@ -15,6 +21,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The URL to a non-GitHub source repo. public string CustomSourceUrl { get; set; } + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + /// The version of the latest unofficial update, if applicable. public ISemanticVersion UnofficialVersion { get; set; } -- cgit From 9945408aa4ea2f6e07c2820f6aa1c160c68423d5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 30 May 2018 21:21:33 -0400 Subject: add summary and 'unofficial' status to wiki client (#532) --- .../Framework/Clients/Wiki/WikiCompatibilityClient.cs | 6 ++++-- .../Framework/Clients/Wiki/WikiCompatibilityEntry.cs | 3 +++ .../Framework/Clients/Wiki/WikiCompatibilityStatus.cs | 11 +++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs index a68a0b4e..d0da42df 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -92,7 +92,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki // parse other fields string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim(); - string[] ids = this.GetAttribute(node, "data-id")?.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; + string summary = node.Descendants("td").FirstOrDefault(p => p.GetAttributeValue("class", null) == "summary")?.InnerText.Trim(); + string[] ids = this.GetAttribute(node, "data-id")?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() ?? new string[0]; int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); string githubRepo = this.GetAttribute(node, "data-github"); @@ -110,7 +111,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, CustomUrl = customUrl, - UnofficialVersion = unofficialVersion + UnofficialVersion = unofficialVersion, + Summary = summary }; } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs index 9aabdcba..8bc66e20 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -29,5 +29,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The compatibility status. public WikiCompatibilityStatus Status { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + public string Summary { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs index 06252b2f..a1d2dfae 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -9,16 +9,19 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The mod is compatible if you use an optional official download. Optional = 1, + /// The mod is compatible if you use an unofficial update. + Unofficial = 2, + /// The mod isn't compatible, but the player can fix it or there's a good alternative. - Workaround = 2, + Workaround = 3, /// The mod isn't compatible. - Broken = 3, + Broken = 4, /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. - Abandoned = 4, + Abandoned = 5, /// The mod is no longer needed and should be removed. - Obsolete = 5 + Obsolete = 6 } } -- cgit From de74b038e4c15d393de8828c89814ef7eaefe507 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 18:22:04 -0400 Subject: move web API client into toolkit (#532) --- src/SMAPI.Internal/Models/ModInfoModel.cs | 56 ----------------- src/SMAPI.Internal/Models/ModSeachModel.cs | 37 ----------- src/SMAPI.Internal/SMAPI.Internal.projitems | 2 - src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 2 +- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI/Constants.cs | 16 ++++- src/SMAPI/Framework/WebApiClient.cs | 73 ---------------------- src/SMAPI/Program.cs | 4 +- src/SMAPI/SemanticVersion.cs | 19 +++--- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Framework/Clients/WebApi/ModInfoModel.cs | 56 +++++++++++++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 37 +++++++++++ .../Framework/Clients/WebApi/WebApiClient.cs | 70 +++++++++++++++++++++ 17 files changed, 192 insertions(+), 191 deletions(-) delete mode 100644 src/SMAPI.Internal/Models/ModInfoModel.cs delete mode 100644 src/SMAPI.Internal/Models/ModSeachModel.cs delete mode 100644 src/SMAPI/Framework/WebApiClient.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Internal/Models/ModInfoModel.cs b/src/SMAPI.Internal/Models/ModInfoModel.cs deleted file mode 100644 index 725c88bb..00000000 --- a/src/SMAPI.Internal/Models/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Internal.Models -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The semantic version for the mod's latest release. - public string Version { get; set; } - - /// The semantic version for the mod's latest preview release, if available and different from . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/SMAPI.Internal/Models/ModSeachModel.cs b/src/SMAPI.Internal/Models/ModSeachModel.cs deleted file mode 100644 index fac72135..00000000 --- a/src/SMAPI.Internal/Models/ModSeachModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Internal.Models -{ - /// Specifies mods whose update-check info to fetch. - internal class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) - { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; - } - } -} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 33b8cbfa..54b12003 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -12,8 +12,6 @@ - - diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index fc90d067..1ec855d5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index bd27d624..4a4a40cd 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 2782e2b9..e6074a60 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index f4abd379..1d7e4fff 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 79fe8f87..4c879c8d 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 87a87ab7..6cac6b8f 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index bdbd5fbb..786c1a39 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.6-beta.15"); + public static ISemanticVersion ApiVersion { get; } /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.13"); + public static ISemanticVersion MinimumGameVersion { get; } /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -70,6 +70,9 @@ namespace StardewModdingAPI /**** ** Internal ****/ + /// SMAPI's current semantic version as a mod toolkit version. + internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } + /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; @@ -104,6 +107,15 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ + /// Initialise the static values. + static Constants() + { + Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.15"); + Constants.MinimumGameVersion = new GameVersion("1.3.13"); + + Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); + } + /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs deleted file mode 100644 index e33b2681..00000000 --- a/src/SMAPI/Framework/WebApiClient.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Newtonsoft.Json; -using StardewModdingAPI.Internal.Models; - -namespace StardewModdingAPI.Framework -{ - /// Provides methods for interacting with the SMAPI web API. - internal class WebApiClient - { - /********* - ** Properties - *********/ - /// The base URL for the web API. - private readonly Uri BaseUrl; - - /// The API version number. - private readonly ISemanticVersion Version; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The base URL for the web API. - /// The web API version. - public WebApiClient(string baseUrl, ISemanticVersion version) - { -#if !SMAPI_FOR_WINDOWS - baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif - this.BaseUrl = new Uri(baseUrl); - this.Version = version; - } - - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) - { - return this.Post>( - $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) - ); - } - - - /********* - ** Private methods - *********/ - /// Fetch the response from the backend API. - /// The body content type. - /// The expected response type. - /// The request URL, optionally excluding the base URL. - /// The body content to post. - private TResult Post(string url, TBody content) - { - /*** - ** Note: avoid HttpClient for Mac compatibility. - ***/ - using (WebClient client = new WebClient()) - { - Uri fullUrl = new Uri(this.BaseUrl, url); - string data = JsonConvert.SerializeObject(content); - - client.Headers["Content-Type"] = "application/json"; - client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; - string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response); - } - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d5f5fdcd..b7b4dfc7 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -26,7 +26,7 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -561,7 +561,7 @@ namespace StardewModdingAPI new Thread(() => { // create client - WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersionForToolkit); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 3ee3ccf3..c4dd1912 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -54,6 +54,13 @@ namespace StardewModdingAPI public SemanticVersion(Version version) : this(new Toolkit.SemanticVersion(version)) { } + /// Construct an instance. + /// The underlying semantic version implementation. + internal SemanticVersion(Toolkit.ISemanticVersion version) + { + this.Version = version; + } + /// Whether this is a pre-release version. public bool IsPrerelease() { @@ -146,17 +153,5 @@ namespace StardewModdingAPI parsed = null; return false; } - - - /********* - ** Private methods - *********/ - /// Construct an instance. - /// The underlying semantic version implementation. - private SemanticVersion(Toolkit.ISemanticVersion version) - { - this.Version = version; - } - } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 96b3aa5b..a5ccc62d 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -279,7 +279,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs new file mode 100644 index 00000000..e1a204c6 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Generic metadata about a mod. + public class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The semantic version for the mod's latest release. + public string Version { get; set; } + + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs new file mode 100644 index 00000000..c0ee34ea --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies mods whose update-check info to fetch. + public class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + /// Whether to allow non-semantic versions, instead of returning an error for those. + public bool AllowInvalidVersions { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The namespaced mod keys to search. + /// Whether to allow non-semantic versions, instead of returning an error for those. + public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + { + this.ModKeys = modKeys.ToArray(); + this.AllowInvalidVersions = allowInvalidVersions; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs new file mode 100644 index 00000000..277fbeeb --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Provides methods for interacting with the SMAPI web API. + internal class WebApiClient + { + /********* + ** Properties + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) + { +#if !SMAPI_FOR_WINDOWS + baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } + + /// Get the latest SMAPI version. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params string[] modKeys) + { + return this.Post>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys, allowInvalidVersions: true) + ); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The body content type. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + /// The body content to post. + private TResult Post(string url, TBody content) + { + // note: avoid HttpClient for Mac compatibility + using (WebClient client = new WebClient()) + { + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response); + } + } + } +} -- cgit From 625c538f244519700f3942b2b2969845db9a99b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Jun 2018 20:22:46 -0400 Subject: move manifest parsing into toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 84 +++++-------- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 1 + src/SMAPI/Framework/ContentPack.cs | 2 +- src/SMAPI/Framework/Exceptions/SParseException.cs | 17 --- src/SMAPI/Framework/InternalExtensions.cs | 16 --- src/SMAPI/Framework/LegacyManifestVersion.cs | 26 ---- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 20 ++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 37 +++--- src/SMAPI/Framework/Models/Manifest.cs | 74 +++++++++-- .../Framework/Models/ManifestContentPackFor.cs | 25 +++- src/SMAPI/Framework/Models/ManifestDependency.cs | 11 +- .../Framework/Serialisation/ColorConverter.cs | 47 +++++++ .../CrossplatformConverters/ColorConverter.cs | 46 ------- .../CrossplatformConverters/PointConverter.cs | 42 ------- .../CrossplatformConverters/RectangleConverter.cs | 51 -------- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 139 --------------------- .../Framework/Serialisation/PointConverter.cs | 43 +++++++ .../Framework/Serialisation/RectangleConverter.cs | 52 ++++++++ .../Serialisation/SimpleReadOnlyConverter.cs | 77 ------------ .../ManifestContentPackForConverter.cs | 50 -------- .../ManifestDependencyArrayConverter.cs | 60 --------- .../SmapiConverters/SemanticVersionConverter.cs | 36 ------ .../SmapiConverters/StringEnumConverter.cs | 22 ---- src/SMAPI/IManifest.cs | 2 +- src/SMAPI/Program.cs | 18 ++- src/SMAPI/StardewModdingAPI.csproj | 20 +-- .../Converters/ManifestContentPackForConverter.cs | 50 ++++++++ .../Converters/ManifestDependencyArrayConverter.cs | 60 +++++++++ .../Converters/SemanticVersionConverter.cs | 36 ++++++ .../Converters/SimpleReadOnlyConverter.cs | 76 +++++++++++ .../Converters/StringEnumConverter.cs | 22 ++++ .../Serialisation/InternalExtensions.cs | 21 ++++ .../Serialisation/JsonHelper.cs | 123 ++++++++++++++++++ .../Serialisation/Models/LegacyManifestVersion.cs | 26 ++++ .../Serialisation/Models/Manifest.cs | 49 ++++++++ .../Serialisation/Models/ManifestContentPackFor.cs | 15 +++ .../Serialisation/Models/ManifestDependency.cs | 35 ++++++ .../Serialisation/SParseException.cs | 17 +++ 38 files changed, 851 insertions(+), 697 deletions(-) delete mode 100644 src/SMAPI/Framework/Exceptions/SParseException.cs delete mode 100644 src/SMAPI/Framework/LegacyManifestVersion.cs create mode 100644 src/SMAPI/Framework/Serialisation/ColorConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/JsonHelper.cs create mode 100644 src/SMAPI/Framework/Serialisation/PointConverter.cs create mode 100644 src/SMAPI/Framework/Serialisation/RectangleConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs create mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index d63eb1a2..2fbeb9b6 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -9,7 +9,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; namespace StardewModdingAPI.Tests.Core { @@ -93,8 +93,8 @@ namespace StardewModdingAPI.Tests.Core Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); - Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); + Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name."); Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match."); @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1"))); + mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1")); this.SetupMetadataForValidation(mock); // act @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Tests.Core public void ValidateManifests_MissingEntryDLL_Fails() { // arrange - Mock mock = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.EntryDll = "Missing.dll"), allowStatusChange: true); + Mock mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true); this.SetupMetadataForValidation(mock); // act @@ -189,7 +189,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - Mock modB = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.Name = "Mod B"), allowStatusChange: true); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true); Mock modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false); foreach (Mock mod in new[] { modA, modB, modC }) this.SetupMetadataForValidation(mod); @@ -398,8 +398,8 @@ namespace StardewModdingAPI.Tests.Core { // arrange // A 1.0 ◀── B (need A 1.1) - 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.1")), allowStatusChange: true); + Mock modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.1") }), allowStatusChange: true); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); @@ -414,8 +414,8 @@ namespace StardewModdingAPI.Tests.Core { // arrange // A 1.0 ◀── B (need A 1.0-beta) - 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-beta")), allowStatusChange: false); + Mock modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0-beta") }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray(); @@ -431,8 +431,8 @@ namespace StardewModdingAPI.Tests.Core { // 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); + Mock modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0")); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }, new ModDatabase()).ToArray(); @@ -448,7 +448,7 @@ namespace StardewModdingAPI.Tests.Core { // 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); + Mock modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }, new ModDatabase()).ToArray(); @@ -463,46 +463,26 @@ namespace StardewModdingAPI.Tests.Core ** Private methods *********/ /// Get a randomised basic manifest. - /// Adjust the generated manifest. - private Manifest GetManifest(Action adjust = null) - { - Manifest manifest = new Manifest - { - Name = Sample.String(), - Author = Sample.String(), - Version = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - Description = Sample.String(), - UniqueID = $"{Sample.String()}.{Sample.String()}", - EntryDll = $"{Sample.String()}.dll" - }; - adjust?.Invoke(manifest); - return manifest; - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The mod version. - /// Adjust the generated manifest. - /// The dependencies this mod requires. - private IManifest GetManifest(string uniqueID, string version, Action adjust, params IManifestDependency[] dependencies) - { - return this.GetManifest(manifest => - { - manifest.Name = uniqueID; - manifest.UniqueID = uniqueID; - manifest.Version = new SemanticVersion(version); - manifest.Dependencies = dependencies; - adjust?.Invoke(manifest); - }); - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The mod version. - /// The dependencies this mod requires. - private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies) + /// The value, or null for a generated value. + /// The value, or null for a generated value. + /// The value, or null for a generated value. + /// The value, or null for a generated value. + /// The value. + /// The value. + /// The value. + private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) { - return this.GetManifest(uniqueID, version, null, dependencies); + return new Manifest( + uniqueID: id ?? $"{Sample.String()}.{Sample.String()}", + name: name ?? id ?? Sample.String(), + author: Sample.String(), + description: Sample.String(), + version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + entryDll: entryDll ?? $"{Sample.String()}.dll", + contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID) : null, + minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, + dependencies: dependencies + ); } /// Get a randomised basic manifest. @@ -518,7 +498,7 @@ namespace StardewModdingAPI.Tests.Core /// Whether the code being tested is allowed to change the mod status. private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) { - IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); + IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); return this.GetMetadata(manifest, allowStatusChange); } diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index f1a72012..b797393b 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; +using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Utilities { diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index ee6df1ec..4a4adb90 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -2,7 +2,7 @@ using System; using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Utilities; using xTile; diff --git a/src/SMAPI/Framework/Exceptions/SParseException.cs b/src/SMAPI/Framework/Exceptions/SParseException.cs deleted file mode 100644 index f7133ee7..00000000 --- a/src/SMAPI/Framework/Exceptions/SParseException.cs +++ /dev/null @@ -1,17 +0,0 @@ -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/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index bff4807c..ff3925fb 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Reflection; using Microsoft.Xna.Framework.Graphics; -using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -91,20 +90,5 @@ 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/LegacyManifestVersion.cs b/src/SMAPI/Framework/LegacyManifestVersion.cs deleted file mode 100644 index 454b9137..00000000 --- a/src/SMAPI/Framework/LegacyManifestVersion.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework -{ - /// An implementation of that hamdles the legacy version format. - internal class LegacyManifestVersion : SemanticVersion - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional build tag. - [JsonConstructor] - public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - : base( - majorVersion, - minorVersion, - patchVersion, - build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation - ) - { } - } -} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index e8726938..18904857 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -5,7 +5,7 @@ using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers @@ -179,16 +179,14 @@ namespace StardewModdingAPI.Framework.ModHelpers 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 } - }; + IManifest manifest = new Manifest( + uniqueID: id, + name: name, + author: author, + description: description, + version: version, + contentPackFor: new ManifestContentPackFor(this.ModID) + ); // create content pack return this.CreateContentPack(directoryPath, manifest); diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index d46caa55..ddc8650c 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Utilities; +using ToolkitManifest = StardewModdingAPI.Toolkit.Serialisation.Models.Manifest; namespace StardewModdingAPI.Framework.ModLoading { @@ -28,25 +28,28 @@ namespace StardewModdingAPI.Framework.ModLoading { // read file Manifest manifest = null; - string path = Path.Combine(modDir.FullName, "manifest.json"); string error = null; - try { - manifest = jsonHelper.ReadJsonFile(path); - if (manifest == null) + string path = Path.Combine(modDir.FullName, "manifest.json"); + try { - error = File.Exists(path) - ? "its manifest is invalid." - : "it doesn't have a manifest."; + ToolkitManifest rawManifest = jsonHelper.ReadJsonFile(path); + if (rawManifest == null) + { + error = File.Exists(path) + ? "its manifest is invalid." + : "it doesn't have a manifest."; + } + manifest = new Manifest(rawManifest); + } + catch (SParseException ex) + { + error = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - } - catch (SParseException ex) - { - error = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } // parse internal data record (if any) diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index f5867cf3..92ffe0dc 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; namespace StardewModdingAPI.Framework.Models { @@ -11,39 +11,87 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// The mod name. - public string Name { get; set; } + public string Name { get; } /// A brief description of the mod. - public string Description { get; set; } + public string Description { get; } /// The mod author's name. - public string Author { get; set; } + public string Author { get; } /// The mod version. - public ISemanticVersion Version { get; set; } + public ISemanticVersion Version { get; } /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; set; } + public ISemanticVersion MinimumApiVersion { get; } /// The name of the DLL in the directory that has the method. Mutually exclusive with . - public string EntryDll { get; set; } + public string EntryDll { get; } /// The mod which will read this as a content pack. Mutually exclusive with . - [JsonConverter(typeof(ManifestContentPackForConverter))] - public IManifestContentPackFor ContentPackFor { get; set; } + public IManifestContentPackFor ContentPackFor { get; } /// The other mods that must be loaded before this mod. - [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public IManifestDependency[] Dependencies { get; set; } + public IManifestDependency[] Dependencies { get; } /// The namespaced mod IDs to query for updates (like Nexus:541). public string[] UpdateKeys { get; set; } /// The unique mod ID. - public string UniqueID { get; set; } + public string UniqueID { get; } /// Any manifest fields which didn't match a valid field. [JsonExtensionData] - public IDictionary ExtraFields { get; set; } + public IDictionary ExtraFields { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The toolkit manifest. + public Manifest(Toolkit.Serialisation.Models.Manifest manifest) + : this( + uniqueID: manifest.UniqueID, + name: manifest.Name, + author: manifest.Author, + description: manifest.Description, + version: manifest.Version != null ? new SemanticVersion(manifest.Version) : null, + entryDll: manifest.EntryDll, + minimumApiVersion: manifest.MinimumApiVersion != null ? new SemanticVersion(manifest.MinimumApiVersion) : null, + contentPackFor: manifest.ContentPackFor != null ? new ManifestContentPackFor(manifest.ContentPackFor) : null, + dependencies: manifest.Dependencies?.Select(p => p != null ? (IManifestDependency)new ManifestDependency(p) : null).ToArray(), + updateKeys: manifest.UpdateKeys, + extraFields: manifest.ExtraFields + ) + { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The name of the DLL in the directory that has the method. Mutually exclusive with . + /// The minimum SMAPI version required by this mod, if any. + /// The modID which will read this as a content pack. Mutually exclusive with . + /// The other mods that must be loaded before this mod. + /// The namespaced mod IDs to query for updates (like Nexus:541). + /// Any manifest fields which didn't match a valid field. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string entryDll = null, ISemanticVersion minimumApiVersion = null, IManifestContentPackFor contentPackFor = null, IManifestDependency[] dependencies = null, string[] updateKeys = null, IDictionary extraFields = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.EntryDll = entryDll; + this.ContentPackFor = contentPackFor; + this.MinimumApiVersion = minimumApiVersion; + this.Dependencies = dependencies ?? new IManifestDependency[0]; + this.UpdateKeys = updateKeys ?? new string[0]; + this.ExtraFields = extraFields; + } } } diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs index 7836bbcc..cdad8893 100644 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -7,9 +7,30 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// The unique ID of the mod which can read this content pack. - public string UniqueID { get; set; } + public string UniqueID { get; } /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } + public ISemanticVersion MinimumVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The toolkit instance. + public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) + { + this.UniqueID = contentPackFor.UniqueID; + this.MinimumVersion = new SemanticVersion(contentPackFor.MinimumVersion); + } + + /// Construct an instance. + /// The unique ID of the mod which can read this content pack. + /// The minimum required version (if any). + public ManifestContentPackFor(string uniqueID, ISemanticVersion minimumVersion = null) + { + this.UniqueID = uniqueID; + this.MinimumVersion = minimumVersion; + } } } diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs index 97f0775a..e92597f3 100644 --- a/src/SMAPI/Framework/Models/ManifestDependency.cs +++ b/src/SMAPI/Framework/Models/ManifestDependency.cs @@ -7,18 +7,23 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// The unique mod ID to require. - public string UniqueID { get; set; } + public string UniqueID { get; } /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } + public ISemanticVersion MinimumVersion { get; } /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; set; } + public bool IsRequired { get; } /********* ** Public methods *********/ + /// Construct an instance. + /// The toolkit instance. + public ManifestDependency(Toolkit.Serialisation.Models.ManifestDependency dependency) + : this(dependency.UniqueID, dependency.MinimumVersion?.ToString(), dependency.IsRequired) { } + /// Construct an instance. /// The unique mod ID to require. /// The minimum required version (if any). diff --git a/src/SMAPI/Framework/Serialisation/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/ColorConverter.cs new file mode 100644 index 00000000..c27065bf --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/ColorConverter.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } + /// - Windows format: "26, 51, 76, 102" + /// + internal class ColorConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Color ReadObject(JObject obj, string path) + { + 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); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Color ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 4) + throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); + + int r = Convert.ToInt32(parts[0]); + int g = Convert.ToInt32(parts[1]); + int b = Convert.ToInt32(parts[2]); + int a = Convert.ToInt32(parts[3]); + return new Color(r, g, b, a); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs deleted file mode 100644 index f1b2f04f..00000000 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } - /// - Windows format: "26, 51, 76, 102" - /// - internal class ColorConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Color ReadObject(JObject obj, string path) - { - 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); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Color ReadString(string str, string path) - { - string[] parts = str.Split(','); - if (parts.Length != 4) - throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); - - int r = Convert.ToInt32(parts[0]); - int g = Convert.ToInt32(parts[1]); - int b = Convert.ToInt32(parts[2]); - int a = Convert.ToInt32(parts[3]); - 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 deleted file mode 100644 index 434b7ea5..00000000 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "X": 1, "Y": 2 } - /// - Windows format: "1, 2" - /// - internal class PointConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Point ReadObject(JObject obj, string path) - { - int x = obj.ValueIgnoreCase(nameof(Point.X)); - int y = obj.ValueIgnoreCase(nameof(Point.Y)); - return new Point(x, y); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Point ReadString(string str, string path) - { - string[] parts = str.Split(','); - if (parts.Length != 2) - throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); - - int x = Convert.ToInt32(parts[0]); - int y = Convert.ToInt32(parts[1]); - return new Point(x, y); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs deleted file mode 100644 index 62bc8637..00000000 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } - /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" - /// - internal class RectangleConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Rectangle ReadObject(JObject obj, string path) - { - 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); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Rectangle ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return Rectangle.Empty; - - 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})."); - - int x = Convert.ToInt32(match.Groups["x"].Value); - int y = Convert.ToInt32(match.Groups["y"].Value); - int width = Convert.ToInt32(match.Groups["width"].Value); - int height = Convert.ToInt32(match.Groups["height"].Value); - - return new Rectangle(x, y, width, height); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs deleted file mode 100644 index 6cba343e..00000000 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Xna.Framework.Input; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.CrossplatformConverters; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Encapsulates SMAPI's JSON file parsing. - internal class JsonHelper - { - /********* - ** Accessors - *********/ - /// The JSON settings to use when serialising and deserialising files. - private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - 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(), - new StringEnumConverter(), - - // crossplatform compatibility - new ColorConverter(), - new PointConverter(), - new RectangleConverter() - } - }; - - - /********* - ** Public methods - *********/ - /// Read a JSON file. - /// The model type. - /// The absolete file path. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - /// The given path is empty or invalid. - public TModel ReadJsonFile(string fullPath) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // read file - string json; - try - { - json = File.ReadAllText(fullPath); - } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) - { - return null; - } - - // deserialise model - try - { - return this.Deserialise(json); - } - catch (Exception ex) - { - string error = $"Can't parse JSON file at {fullPath}."; - - if (ex is JsonReaderException) - { - error += " This doesn't seem to be valid JSON."; - if (json.Contains("“") || json.Contains("”")) - error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; - } - error += $"\nTechnical details: {ex.Message}"; - throw new JsonReaderException(error); - } - } - - /// Save to a JSON file. - /// The model type. - /// The absolete file path. - /// The model to save. - /// The given path is empty or invalid. - public void WriteJsonFile(string fullPath, TModel model) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // create directory if needed - string dir = Path.GetDirectoryName(fullPath); - if (dir == null) - throw new ArgumentException("The file path is invalid.", nameof(fullPath)); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - // write file - string json = JsonConvert.SerializeObject(model, this.JsonSettings); - File.WriteAllText(fullPath, json); - } - - - /********* - ** Private methods - *********/ - /// Deserialize JSON text if possible. - /// The model type. - /// The raw JSON text. - private TModel Deserialise(string json) - { - try - { - return JsonConvert.DeserializeObject(json, this.JsonSettings); - } - catch (JsonReaderException) - { - // try replacing curly quotes - if (json.Contains("“") || json.Contains("”")) - { - try - { - return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); - } - catch { /* rethrow original error */ } - } - - throw; - } - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/PointConverter.cs b/src/SMAPI/Framework/Serialisation/PointConverter.cs new file mode 100644 index 00000000..fbc857d2 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/PointConverter.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// + internal class PointConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Point ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Point.X)); + int y = obj.ValueIgnoreCase(nameof(Point.Y)); + return new Point(x, y); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Point ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(parts[0]); + int y = Convert.ToInt32(parts[1]); + return new Point(x, y); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs new file mode 100644 index 00000000..4f55cc32 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } + /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" + /// + internal class RectangleConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Rectangle ReadObject(JObject obj, string path) + { + 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); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Rectangle ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return Rectangle.Empty; + + 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})."); + + int x = Convert.ToInt32(match.Groups["x"].Value); + int y = Convert.ToInt32(match.Groups["y"].Value); + int width = Convert.ToInt32(match.Groups["width"].Value); + int height = Convert.ToInt32(match.Groups["height"].Value); + + return new Rectangle(x, y, width, height); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs b/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs deleted file mode 100644 index 5765ad96..00000000 --- a/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// The base implementation for simplified converters which deserialise without overriding serialisation. - /// The type to deserialise. - internal abstract class SimpleReadOnlyConverter : 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(T); - } - - /// 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."); - } - - /// Reads 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) - { - string path = reader.Path; - switch (reader.TokenType) - { - case JsonToken.StartObject: - return this.ReadObject(JObject.Load(reader), path); - case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value(), path); - default: - throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); - } - } - - - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected virtual T ReadObject(JObject obj, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected virtual T ReadString(string str, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs deleted file mode 100644 index af7558f6..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -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/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs deleted file mode 100644 index 4150d5fb..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs +++ /dev/null @@ -1,60 +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 ManifestDependencyArrayConverter : 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(IManifestDependency[]); - } - - - /********* - ** 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 (JObject obj in JArray.Load(reader).Children()) - { - 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(); - } - - /// 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/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs deleted file mode 100644 index 7ee7e29b..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) - { - 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); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) - { - 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 (path: {path})."); - return version; - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs deleted file mode 100644 index c88ac834..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Newtonsoft.Json.Converters; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// A variant of which only converts a specified enum. - /// The enum type. - internal class StringEnumConverter : StringEnumConverter - { - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type type) - { - return - base.CanConvert(type) - && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); - } - } -} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs index 183ac105..6c07d374 100644 --- a/src/SMAPI/IManifest.cs +++ b/src/SMAPI/IManifest.cs @@ -36,7 +36,7 @@ namespace StardewModdingAPI IManifestDependency[] Dependencies { get; } /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; set; } + string[] UpdateKeys { get; } /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 5c5137ef..e55a96b2 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -10,6 +10,7 @@ using System.Runtime.ExceptionServices; using System.Security; using System.Text.RegularExpressions; using System.Threading; +using Microsoft.Xna.Framework.Input; #if SMAPI_FOR_WINDOWS using System.Windows.Forms; #endif @@ -27,8 +28,11 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; +using Keys = System.Windows.Forms.Keys; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; using ThreadState = System.Threading.ThreadState; @@ -148,6 +152,18 @@ namespace StardewModdingAPI }; this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + // init JSON parser + JsonConverter[] converters = { + new StringEnumConverter(), + new StringEnumConverter(), + new StringEnumConverter(), + new ColorConverter(), + new PointConverter(), + new RectangleConverter() + }; + foreach (JsonConverter converter in converters) + this.JsonHelper.JsonSettings.Converters.Add(converter); + // hook up events ContentEvents.Init(this.EventManager); ControlEvents.Init(this.EventManager); @@ -1093,7 +1109,7 @@ namespace StardewModdingAPI /// The mods for which to reload translations. private void ReloadTranslations(IEnumerable mods) { - JsonHelper jsonHelper = new JsonHelper(); + JsonHelper jsonHelper = this.JsonHelper; foreach (IModMetadata metadata in mods) { if (metadata.IsContentPack) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a5ccc62d..3c953ec5 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -103,6 +103,9 @@ + + + @@ -114,16 +117,17 @@ + + + - - @@ -151,13 +155,6 @@ - - - - - - - @@ -235,16 +232,12 @@ - - - - @@ -283,7 +276,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs new file mode 100644 index 00000000..232c22a7 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of arrays. + public 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(ManifestContentPackFor[]); + } + + + /********* + ** 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/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs new file mode 100644 index 00000000..0a304ee3 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of arrays. + internal class ManifestDependencyArrayConverter : 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(ManifestDependency[]); + } + + + /********* + ** 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 (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.ValueIgnoreCase(nameof(ManifestDependency.UniqueID)); + string minVersion = obj.ValueIgnoreCase(nameof(ManifestDependency.MinimumVersion)); + bool required = obj.ValueIgnoreCase(nameof(ManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); + } + 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/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs new file mode 100644 index 00000000..2075d27e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of . + internal class SemanticVersionConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override SemanticVersion ReadObject(JObject obj, string path) + { + int major = obj.ValueIgnoreCase("MajorVersion"); + int minor = obj.ValueIgnoreCase("MinorVersion"); + int patch = obj.ValueIgnoreCase("PatchVersion"); + string build = obj.ValueIgnoreCase("Build"); + return new LegacyManifestVersion(major, minor, patch, build); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override SemanticVersion ReadString(string str, string path) + { + 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 (path: {path})."); + return (SemanticVersion)version; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs new file mode 100644 index 00000000..5e0b0f4a --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs @@ -0,0 +1,76 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// The base implementation for simplified converters which deserialise without overriding serialisation. + /// The type to deserialise. + internal abstract class SimpleReadOnlyConverter : 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(T); + } + + /// 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."); + } + + /// Reads 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) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader), path); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected virtual T ReadObject(JObject obj, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected virtual T ReadString(string str, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs new file mode 100644 index 00000000..13e6e3a1 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// A variant of which only converts a specified enum. + /// The enum type. + internal class StringEnumConverter : StringEnumConverter + { + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type type) + { + return + base.CanConvert(type) + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs b/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs new file mode 100644 index 00000000..12b2c933 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// Provides extension methods for parsing JSON. + public static class JsonExtensions + { + /// 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/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs new file mode 100644 index 00000000..00f334ad --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// Encapsulates SMAPI's JSON file parsing. + public class JsonHelper + { + /********* + ** Accessors + *********/ + /// The JSON settings to use when serialising and deserialising files. + public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded + Converters = new List { new SemanticVersionConverter() } + }; + + + /********* + ** Public methods + *********/ + /// Read a JSON file. + /// The model type. + /// The absolete file path. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The given path is empty or invalid. + public TModel ReadJsonFile(string fullPath) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // read file + string json; + try + { + json = File.ReadAllText(fullPath); + } + catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + { + return null; + } + + // deserialise model + try + { + return this.Deserialise(json); + } + catch (Exception ex) + { + string error = $"Can't parse JSON file at {fullPath}."; + + if (ex is JsonReaderException) + { + error += " This doesn't seem to be valid JSON."; + if (json.Contains("“") || json.Contains("”")) + error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + } + error += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(error); + } + } + + /// Save to a JSON file. + /// The model type. + /// The absolete file path. + /// The model to save. + /// The given path is empty or invalid. + public void WriteJsonFile(string fullPath, TModel model) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // create directory if needed + string dir = Path.GetDirectoryName(fullPath); + if (dir == null) + throw new ArgumentException("The file path is invalid.", nameof(fullPath)); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + // write file + string json = JsonConvert.SerializeObject(model, this.JsonSettings); + File.WriteAllText(fullPath, json); + } + + + /********* + ** Private methods + *********/ + /// Deserialize JSON text if possible. + /// The model type. + /// The raw JSON text. + private TModel Deserialise(string json) + { + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException) + { + // try replacing curly quotes + if (json.Contains("“") || json.Contains("”")) + { + try + { + return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); + } + catch { /* rethrow original error */ } + } + + throw; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs new file mode 100644 index 00000000..12f6755b --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// An implementation of that hamdles the legacy version format. + public class LegacyManifestVersion : SemanticVersion + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional build tag. + [JsonConstructor] + public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) + : base( + majorVersion, + minorVersion, + patchVersion, + build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation + ) + { } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs new file mode 100644 index 00000000..68987dd1 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// A manifest which describes a mod for SMAPI. + public class Manifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + public SemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + public SemanticVersion MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the Entry 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 ManifestContentPackFor ContentPackFor { get; set; } + + /// The other mods that must be loaded before this mod. + [JsonConverter(typeof(ManifestDependencyArrayConverter))] + public ManifestDependency[] Dependencies { get; set; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..00546533 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public class ManifestContentPackFor + { + /********* + ** 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 SemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs new file mode 100644 index 00000000..d902f9ac --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -0,0 +1,35 @@ +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// A mod dependency listed in a mod manifest. + public class ManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public SemanticVersion MinimumVersion { get; set; } + + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique mod ID to require. + /// The minimum required version (if any). + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) + { + this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; + this.IsRequired = required; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs b/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs new file mode 100644 index 00000000..61a7b305 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// 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) { } + } +} -- cgit From 570b19ca7a16dc6af901a8869f178a8196147fb0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Jun 2018 21:38:24 -0400 Subject: tweak client for reuse in toolkit (#532) --- src/SMAPI/Program.cs | 6 +++- .../Framework/Clients/WebApi/WebApiClient.cs | 34 +++++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e55a96b2..05a3134e 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -577,7 +577,11 @@ namespace StardewModdingAPI new Thread(() => { // create client - WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersionForToolkit); + string url = this.Settings.WebApiBaseUrl; +#if !SMAPI_FOR_WINDOWS + url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + WebApiClient client = new WebApiClient(url, Constants.ApiVersionForToolkit); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 277fbeeb..d94b0259 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using Newtonsoft.Json; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Provides methods for interacting with the SMAPI web API. - internal class WebApiClient + public class WebApiClient { /********* ** Properties @@ -26,9 +27,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The web API version. public WebApiClient(string baseUrl, ISemanticVersion version) { -#if !SMAPI_FOR_WINDOWS - baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif this.BaseUrl = new Uri(baseUrl); this.Version = version; } @@ -43,6 +41,34 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ); } + /// Get the latest version for a mod. + /// The update keys to search. + public ISemanticVersion GetLatestVersion(string[] updateKeys) + { + if (!updateKeys.Any()) + return null; + + // fetch update results + ModInfoModel[] results = this + .GetModInfo(updateKeys) + .Values + .Where(p => p.Error == null) + .ToArray(); + if (!results.Any()) + return null; + + ISemanticVersion latest = null; + foreach (ModInfoModel result in results) + { + if (!SemanticVersion.TryParse(result.PreviewVersion ?? result.Version, out ISemanticVersion cur)) + continue; + + if (latest == null || cur.IsNewerThan(latest)) + latest = cur; + } + return latest; + } + /********* ** Private methods -- cgit From 6eba10948bf39d5e05505ec060f6920f84610d58 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Jun 2018 23:03:26 -0400 Subject: fix version parsing issues in new toolkit code (#532) --- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 17 --------- .../Framework/Models/ManifestContentPackFor.cs | 2 +- .../Serialisation/SemanticVersionConverter.cs | 40 ++++++++++++++++++++++ src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 1 + .../Converters/SemanticVersionConverter.cs | 5 ++- .../Serialisation/Models/LegacyManifestVersion.cs | 26 -------------- 7 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index b797393b..feab452a 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; -using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Utilities { @@ -272,22 +271,6 @@ namespace StardewModdingAPI.Tests.Utilities Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); } - /**** - ** LegacyManifestVersion - ****/ - [Test(Description = "Assert that the LegacyManifestVersion subclass correctly parses legacy manifest versions.")] - [TestCase(1, 0, 0, null, ExpectedResult = "1.0")] - [TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] - [TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] - [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] - [TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3")] // special case: drop '0' tag for legacy manifest versions - [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] - [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] - public string LegacyManifestVersion(int major, int minor, int patch, string tag) - { - return new LegacyManifestVersion(major, minor, patch, tag).ToString(); - } - /********* ** Private methods diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs index cdad8893..90e20c6a 100644 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Models public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) { this.UniqueID = contentPackFor.UniqueID; - this.MinimumVersion = new SemanticVersion(contentPackFor.MinimumVersion); + this.MinimumVersion = contentPackFor.MinimumVersion != null ? new SemanticVersion(contentPackFor.MinimumVersion) : null; } /// Construct an instance. diff --git a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs new file mode 100644 index 00000000..3e05a440 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of . + internal class SemanticVersionConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override ISemanticVersion ReadObject(JObject obj, string path) + { + int major = obj.ValueIgnoreCase("MajorVersion"); + int minor = obj.ValueIgnoreCase("MinorVersion"); + int patch = obj.ValueIgnoreCase("PatchVersion"); + string build = obj.ValueIgnoreCase("Build"); + if (build == "0") + build = null; // '0' from incorrect examples in old SMAPI documentation + + return new SemanticVersion(major, minor, patch, build); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override ISemanticVersion ReadString(string str, string path) + { + 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 (path: {path})."); + return version; + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 05a3134e..cc59c0cd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -159,7 +159,8 @@ namespace StardewModdingAPI new StringEnumConverter(), new ColorConverter(), new PointConverter(), - new RectangleConverter() + new RectangleConverter(), + new Framework.Serialisation.SemanticVersionConverter() }; foreach (JsonConverter converter in converters) this.JsonHelper.JsonSettings.Converters.Add(converter); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 3c953ec5..7eb5f95a 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -121,6 +121,7 @@ + diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 2075d27e..2ddaa1bf 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -18,7 +18,10 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters int minor = obj.ValueIgnoreCase("MinorVersion"); int patch = obj.ValueIgnoreCase("PatchVersion"); string build = obj.ValueIgnoreCase("Build"); - return new LegacyManifestVersion(major, minor, patch, build); + if (build == "0") + build = null; // '0' from incorrect examples in old SMAPI documentation + + return new SemanticVersion(major, minor, patch, build); } /// Read a JSON string. diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs deleted file mode 100644 index 12f6755b..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/LegacyManifestVersion.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// An implementation of that hamdles the legacy version format. - public class LegacyManifestVersion : SemanticVersion - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional build tag. - [JsonConstructor] - public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - : base( - majorVersion, - minorVersion, - patchVersion, - build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation - ) - { } - } -} -- cgit From 53a6833ab22fb41e909ed8ef50aa9262735818d9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jun 2018 00:16:39 -0400 Subject: return file versions from Nexus in web API (#532) --- docs/release-notes.md | 1 + src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs | 4 ++ .../Clients/Nexus/NexusWebScrapeClient.cs | 44 ++++++++++++++++++++-- .../Framework/ConfigModels/ApiClientsConfig.cs | 5 ++- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/Startup.cs | 3 +- src/SMAPI.Web/appsettings.json | 1 + .../Framework/Clients/WebApi/ModInfoModel.cs | 4 +- 8 files changed, 55 insertions(+), 9 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 39b431cf..11a2a04c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -59,6 +59,7 @@ * For SMAPI developers: * Added more consistent crossplatform handling using a new `EnvironmentUtility`, including MacOS detection. * Added beta update channel to SMAPI, the web API, and home page. + * Added latest mod file version (including optional files) to Nexus API results. * Added more stylish pufferchick on the home page. * Split mod DB out of `StardewModdingAPI.config.json` into its own file. * Rewrote input suppression using new SDV 1.3 APIs. diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs index cd52c72b..4ecf2f76 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { @@ -14,6 +15,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// The mod's semantic version number. public string Version { get; set; } + /// The latest file version. + public ISemanticVersion LatestFileVersion { get; set; } + /// The mod's web URL. [JsonProperty("mod_page_uri")] public string Url { get; set; } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs index d0597965..df5a437d 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { @@ -12,9 +15,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /********* ** Properties *********/ - /// The URL for a Nexus web page excluding the base URL, where {0} is the mod ID. + /// The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID. private readonly string ModUrlFormat; + /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. + public string ModScrapeUrlFormat { get; set; } + /// The underlying HTTP client. private readonly IClient Client; @@ -25,10 +31,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// Construct an instance. /// The user agent for the Nexus Mods API client. /// The base URL for the Nexus Mods site. - /// The URL for a Nexus Mods web page excluding the , where {0} is the mod ID. - public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat) + /// The URL for a Nexus Mods mod page for the user, excluding the , where {0} is the mod ID. + /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. + public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat) { this.ModUrlFormat = modUrlFormat; + this.ModScrapeUrlFormat = modScrapeUrlFormat; this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); } @@ -42,7 +50,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus try { html = await this.Client - .GetAsync(string.Format(this.ModUrlFormat, id)) + .GetAsync(string.Format(this.ModScrapeUrlFormat, id)) .AsString(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -76,10 +84,38 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim(); string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); + // extract file versions + List rawVersions = new List(); + foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) + { + string sectionName = fileSection.Descendants("h2").First().InnerText; + if (sectionName != "Main files" && sectionName != "Optional files") + continue; + + rawVersions.AddRange( + from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version")) + from versionStat in statBox.Descendants().Where(p => p.HasClass("stat")) + select versionStat.InnerText.Trim() + ); + } + + // choose latest file version + ISemanticVersion latestFileVersion = null; + foreach (string rawVersion in rawVersions) + { + if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) + continue; + + if (latestFileVersion == null || cur.IsNewerThan(latestFileVersion)) + latestFileVersion = cur; + } + + // yield info return new NexusMod { Name = name, Version = version, + LatestFileVersion = latestFileVersion, Url = url }; } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index 9452fdf9..ae8f18d2 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -50,9 +50,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The base URL for the Nexus Mods API. public string NexusBaseUrl { get; set; } - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + /// The URL for a Nexus mod page for the user, excluding the , where {0} is the mod ID. public string NexusModUrlFormat { get; set; } + /// The URL for a Nexus mod page to scrape for versions, excluding the , where {0} is the mod ID. + public string NexusModScrapeUrlFormat { get; set; } + /**** ** Pastebin ****/ diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 6cac6b8f..4afcda10 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return new ModInfoModel("Found no mod with this ID."); if (mod.Error != null) return new ModInfoModel(mod.Error); - return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); + return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 2019d6db..58584348 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -85,7 +85,8 @@ namespace StardewModdingAPI.Web services.AddSingleton(new NexusWebScrapeClient( userAgent: userAgent, baseUrl: api.NexusBaseUrl, - modUrlFormat: api.NexusModUrlFormat + modUrlFormat: api.NexusModUrlFormat, + modScrapeUrlFormat: api.NexusModScrapeUrlFormat )); services.AddSingleton(new PastebinClient( diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index fda77183..2f87dbe5 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -32,6 +32,7 @@ "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/", "NexusModUrlFormat": "mods/{0}", + "NexusModScrapeUrlFormat": "mods/{0}?tab=files", "PastebinBaseUrl": "https://pastebin.com/", "PastebinUserKey": null, // see top note diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs index e1a204c6..c8e296f0 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs @@ -9,10 +9,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod name. public string Name { get; set; } - /// The semantic version for the mod's latest release. + /// The mod's latest release number. public string Version { get; set; } - /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's latest optional release, if newer than . public string PreviewVersion { get; set; } /// The mod's web URL. -- cgit From 86a3f8dd460f329fad903770231016813e750168 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 8 Jun 2018 18:46:58 -0400 Subject: allow launching multiple instances without manually changing log path (#494) --- docs/release-notes.md | 1 + docs/technical-docs.md | 1 - src/SMAPI.Installer/InteractiveInstaller.cs | 1 + src/SMAPI/Constants.cs | 7 ++- src/SMAPI/Program.cs | 65 ++++++++++++++++------ .../Utilities/FileUtilities.cs | 46 +++++++++++++++ 6 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 11a2a04c..b46e345f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,6 +29,7 @@ * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. + * Added support for launching multiple instances transparently. This removes the former `--log-path` command-line argument. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index a988eefc..f4358e31 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -137,7 +137,6 @@ change without warning. argument | purpose -------- | ------- -`--log-path "path"` | The relative or absolute path of the log file SMAPI should write. `--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) ### Compile flags diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 6e4cb95d..02dd6891 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -518,6 +518,7 @@ namespace StardewModdingApi.Installer /// Delete a file or folder regardless of file permissions, and block until deletion completes. /// The file or folder to reset. + /// This method is mirred from FileUtilities.ForceDelete in the toolkit. private void ForceDelete(FileSystemInfo entry) { // ignore if already deleted diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 786c1a39..867e01ea 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -82,8 +82,11 @@ namespace StardewModdingAPI /// The file path for the SMAPI metadata file. internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); - /// The file path to the log where the latest output should be saved. - internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); + /// The filename prefix for SMAPI log files. + internal static string LogNamePrefix { get; } = "SMAPI-latest"; + + /// The filename extension for SMAPI log files. + internal static string LogNameExtension { get; } = "txt"; /// A copy of the log leading up to the previous fatal crash, if any. internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index cc59c0cd..570a3c55 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -117,30 +117,19 @@ namespace StardewModdingAPI // get flags from arguments bool writeToConsole = !args.Contains("--no-terminal"); - // get log path from arguments - string logPath = null; - { - int pathIndex = Array.LastIndexOf(args, "--log-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - { - logPath = args[pathIndex]; - if (!Path.IsPathRooted(logPath)) - logPath = Path.Combine(Constants.LogDir, logPath); - } - } - if (string.IsNullOrWhiteSpace(logPath)) - logPath = Constants.DefaultLogPath; - // load SMAPI - using (Program program = new Program(writeToConsole, logPath)) + using (Program program = new Program(writeToConsole)) program.RunInteractively(); } /// Construct an instance. /// Whether to output log messages to the console. - /// The full file path to which to write log messages. - public Program(bool writeToConsole, string logPath) + public Program(bool writeToConsole) { + // init log file + this.PurgeLogFiles(); + string logPath = this.GetLogPath(); + // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); @@ -1258,5 +1247,47 @@ namespace StardewModdingAPI if (this.Settings.VerboseLogging) this.Monitor.Log(message, LogLevel.Trace); } + + /// Get the absolute path to the next available log file. + private string GetLogPath() + { + // default path + { + FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.{Constants.LogNameExtension}")); + if (!defaultFile.Exists) + return defaultFile.FullName; + } + + // get first disambiguated path + for (int i = 2; i < int.MaxValue; i++) + { + FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.player-{i}.{Constants.LogNameExtension}")); + if (!file.Exists) + return file.FullName; + } + + // should never happen + throw new InvalidOperationException("Could not find an available log path."); + } + + /// Delete all log files created by SMAPI. + private void PurgeLogFiles() + { + DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); + foreach (FileInfo logFile in logsDir.EnumerateFiles("*.txt")) + { + if (logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + FileUtilities.ForceDelete(logFile); + } + catch (Exception ex) + { + // leave file if it's locked + } + } + } + } } } diff --git a/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs new file mode 100644 index 00000000..7856fdb1 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs @@ -0,0 +1,46 @@ +using System.IO; +using System.Threading; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides utilities for dealing with files. + public static class FileUtilities + { + /********* + ** Public methods + *********/ + /// Delete a file or folder regardless of file permissions, and block until deletion completes. + /// The file or folder to reset. + public static void ForceDelete(FileSystemInfo entry) + { + // ignore if already deleted + entry.Refresh(); + if (!entry.Exists) + return; + + // delete children + if (entry is DirectoryInfo folder) + { + foreach (FileSystemInfo child in folder.GetFileSystemInfos()) + FileUtilities.ForceDelete(child); + } + + // reset permissions & delete + entry.Attributes = FileAttributes.Normal; + entry.Delete(); + + // wait for deletion to finish + for (int i = 0; i < 10; i++) + { + entry.Refresh(); + if (entry.Exists) + Thread.Sleep(500); + } + + // throw exception if deletion didn't happen before timeout + entry.Refresh(); + if (entry.Exists) + throw new IOException($"Timed out trying to delete {entry.FullName}"); + } + } +} -- cgit From 4b82b111e7392e695b0e021efac2385c1b9850af Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Jun 2018 21:50:24 -0400 Subject: improve semantic version validation --- docs/release-notes.md | 1 + src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 14 ++++++++++++ src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 27 ++++++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index a343cff3..152037f8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -46,6 +46,7 @@ * Fixed some common non-mod build output being included in release zip. * Fixed mods able to intercept other mods' assets via the internal asset keys. * Fixed mods able to indirectly change other mods' data through shared content caches. + * Fixed `SemanticVersion` allowing invalid versions in some cases. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten. diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index feab452a..9091ef09 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -49,6 +49,19 @@ namespace StardewModdingAPI.Tests.Utilities return version.ToString(); } + [Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")] + [TestCase(0, 0, 0, null)] + [TestCase(-1, 0, 0, null)] + [TestCase(0, -1, 0, null)] + [TestCase(0, 0, -1, null)] + [TestCase(1, 0, 0, "-tag")] + [TestCase(1, 0, 0, "tag spaces")] + [TestCase(1, 0, 0, "tag~")] + public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string tag) + { + this.AssertAndLogException(() => new SemanticVersion(major, minor, patch, tag)); + } + [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")] [TestCase(1, 0, 0, ExpectedResult = "1.0")] [TestCase(1, 2, 3, ExpectedResult = "1.2.3")] @@ -79,6 +92,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("1.2.3.apple")] [TestCase("1..2..3")] [TestCase("1.2.3-")] + [TestCase("1.2.3--some-tag")] [TestCase("1.2.3-some-tag...")] [TestCase("1.2.3-some-tag...4")] [TestCase("apple")] diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index bd85f990..3008a4d6 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -10,8 +10,11 @@ namespace StardewModdingAPI.Toolkit /********* ** Properties *********/ + /// A regex pattern matching a valid prerelease tag. + internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; + /// A regex pattern matching a version within a larger string. - internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?"; + internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?" + SemanticVersion.TagPattern + "))?"; /// A regular expression matching a semantic version string. /// @@ -54,6 +57,8 @@ namespace StardewModdingAPI.Toolkit this.Minor = minor; this.Patch = patch; this.Tag = this.GetNormalisedTag(tag); + + this.AssertValid(); } /// Construct an instance. @@ -67,6 +72,8 @@ namespace StardewModdingAPI.Toolkit this.Major = version.Major; this.Minor = version.Minor; this.Patch = version.Build; + + this.AssertValid(); } /// Construct an instance. @@ -87,6 +94,8 @@ namespace StardewModdingAPI.Toolkit this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + + this.AssertValid(); } /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. @@ -235,5 +244,21 @@ namespace StardewModdingAPI.Toolkit // fallback (this should never happen) return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); } + + /// Assert that the current version is valid. + private void AssertValid() + { + if (this.Major < 0 || this.Minor < 0 || this.Patch < 0) + throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); + if (this.Major == 0 && this.Minor == 0 && this.Patch == 0) + throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); + if (this.Tag != null) + { + if (this.Tag.Trim() == "") + throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); + if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$")) + throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); + } + } } } -- cgit From c41e1ed5c16d5402fa373267095e28121b4055f2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Jun 2018 01:03:27 -0400 Subject: fix new validation not allowing capitals in semver tags --- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 2 ++ src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 9091ef09..35d74b60 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -22,6 +22,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")] [TestCase("1.2-some-tag.4", ExpectedResult = "1.2-some-tag.4")] [TestCase("1.2.3-some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] + [TestCase("1.2.3-SoME-tAg.4", ExpectedResult = "1.2.3-SoME-tAg.4")] [TestCase("1.2.3-some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] public string Constructor_FromString(string input) { @@ -35,6 +36,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] [TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3-0")] [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] + [TestCase(1, 2, 3, "sOMe-TaG.4", ExpectedResult = "1.2.3-sOMe-TaG.4")] [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] public string Constructor_FromParts(int major, int minor, int patch, string tag) { diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index 3008a4d6..de480416 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -256,7 +256,7 @@ namespace StardewModdingAPI.Toolkit { if (this.Tag.Trim() == "") throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); - if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$")) + if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); } } -- cgit From d401aff3307f6e2e1641610fdd912b572d6b04c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:10:15 -0400 Subject: rewrite update checks (#551) --- docs/release-notes.md | 14 +- docs/technical-docs.md | 43 ++--- src/SMAPI.Web/Controllers/ModsApiController.cs | 180 +++++++++++++++------ .../Framework/ModRepositories/ModInfoModel.cs | 56 +++++++ src/SMAPI/Framework/IModMetadata.cs | 18 +-- src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 7 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 25 +-- .../Framework/ModUpdateChecking/ModUpdateStatus.cs | 37 ----- src/SMAPI/Program.cs | 143 +++++++--------- src/SMAPI/StardewModdingAPI.csproj | 1 - .../Framework/Clients/WebApi/ModEntryModel.cs | 30 ++++ .../Framework/Clients/WebApi/ModInfoModel.cs | 56 ------- .../Framework/Clients/WebApi/ModSeachModel.cs | 15 +- .../Clients/WebApi/ModSearchEntryModel.cs | 34 ++++ .../Framework/Clients/WebApi/WebApiClient.cs | 39 +---- 15 files changed, 366 insertions(+), 332 deletions(-) create mode 100644 src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs delete mode 100644 src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index df832c34..b3ab2481 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,8 @@ * Added prompt when in beta channel and a new version is found. * Added friendly error when game can't start audio. * Added console warning for mods which don't have update checks configured. + * Added update checks for optional mod files on Nexus. + * Added `player_add name` command, which lets you add items to your inventory by name instead of ID. * Improved how mod warnings are shown in the console. * Fixed `SEHException` errors and performance issues in some cases. * Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`. @@ -16,6 +18,8 @@ * Fixed installer not removing some SMAPI files. * Fixed `smapi.io/install` not linking to a useful page. * Fixed `world_setseason` command not running season-change logic. + * Fixed `world_setseason` command not normalising the season value. + * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * Fixed mod update checks failing if a mod only has prerelease versions on GitHub. * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Fixed Nexus mod update alerts not showing HTTPS links. @@ -38,6 +42,7 @@ * Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map. * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. + * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. @@ -54,14 +59,9 @@ * Mods can't intercept chatbox input. * Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.) -* In console commands: - * Added `player_add name`, which lets you add items to your inventory by name instead of ID. - * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). - * Fixed `world_setseason` not normalising the season value. - * For the web UI: - * Improved log parser design to make it more intuitive. - * Improved layout on small screens. + * Redesigned log parser to make it more intuitive. + * Redesigned UI to be more mobile-friendly. * Added option to download from Nexus. * Changed log parser filters to show `DEBUG` messages by default. * Fixed log parser issue when content packs have no description. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index f4358e31..bdb731d1 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -161,7 +161,7 @@ The log parser lives at https://log.smapi.io. ### Mods API The mods API provides version info for mods hosted by Chucklefish, GitHub, or Nexus Mods. It's used by SMAPI to perform update checks. The `{version}` URL token is the version of SMAPI making the -request; it doesn't do anything currently, but lets us version breaking changes if needed. +request, and is used when needed for backwards compatibility. Each mod is identified by a repository key and unique identifier (like `nexus:541`). The following repositories are supported: @@ -173,32 +173,37 @@ key | repository `nexus` | A mod page on [Nexus Mods](https://www.nexusmods.com/stardewvalley), identified by the mod ID in the page URL. -The API accepts either `GET` or `POST` for convenience: -> ``` ->GET https://api.smapi.io/v2.0/mods?modKeys=nexus:541,chucklefish:4228 ->``` - +The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and +update keys. >``` >POST https://api.smapi.io/v2.0/mods >{ -> "ModKeys": [ "nexus:541", "chucklefish:4228" ] +> "mods": [ +> { +> "id": "Pathoschild.LookupAnything", +> "updateKeys": [ "nexus:541", "chucklefish:4250" ] +> } +> ] >} >``` -It returns a response like this: +The API will automatically aggregate versions and errors, and return a response like this. The +latest version is the main mod version (e.g. 'latest version' field on Nexus); if available and +newer, the latest optional version will be shown as the 'preview version'. >``` >{ -> "chucklefish:4228": { -> "name": "Entoarox Framework", -> "version": "1.8.0", -> "url": "https://community.playstarbound.com/resources/4228" -> }, -> "nexus:541": { -> "name": "Lookup Anything", -> "version": "1.16", -> "url": "http://www.nexusmods.com/stardewvalley/mods/541" -> } ->} +> "Pathoschild.LookupAnything": { +> "id": "Pathoschild.LookupAnything", +> "name": "Lookup Anything", +> "version": "1.18", +> "url": "https://www.nexusmods.com/stardewvalley/mods/541", +> "previewVersion": "1.19-beta", +> "previewUrl": "https://www.nexusmods.com/stardewvalley/mods/541", +> "errors": [ +> "The update key 'chucklefish:4250' matches a mod with invalid semantic version '*'." +> ] +> } +} >``` ## Development diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1ec855d5..c5a1705d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -67,70 +68,89 @@ namespace StardewModdingAPI.Web.Controllers } /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - /// Whether to allow non-semantic versions, instead of returning an error for those. - [HttpGet] - public async Task> GetAsync(string modKeys, bool allowInvalidVersions = false) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. + /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) + public async Task> PostAsync([FromBody] ModSearchModel model) { - // parse model - bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) + ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (ModSearchEntryModel mod in searchMods) { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + if (string.IsNullOrWhiteSpace(mod.ID)) continue; - } - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + // get latest versions + ModEntryModel result = new ModEntryModel { ID = mod.ID }; + IList errors = new List(); + foreach (string updateKey in mod.UpdateKeys ?? new string[0]) { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) + { + errors.Add(data.Error); + continue; + } - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - // fetch info - ModInfoModel info = await repository.GetModInfoAsync(modID); + // handle main version + if (data.Version != null) + { + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); + continue; + } + + if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + { + result.Name = data.Name; + result.Url = data.Url; + result.Version = version.ToString(); + } + } - // validate - if (info.Error == null) + // handle optional version + if (data.PreviewVersion != null) { - if (info.Version == null) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); - if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; + } + + if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + { + result.Name = result.Name ?? data.Name; + result.PreviewUrl = data.Url; + result.PreviewVersion = version.ToString(); + } } + } + + // fallback to preview if latest is invalid + if (result.Version == null && result.PreviewVersion != null) + { + result.Version = result.PreviewVersion; + result.Url = result.PreviewUrl; + result.PreviewVersion = null; + result.PreviewUrl = null; + } + + // special cases + if (mod.ID == "Pathoschild.SMAPI") + { + result.Name = "SMAPI"; + result.Url = "https://smapi.io/"; + if (result.PreviewUrl != null) + result.PreviewUrl = "https://smapi.io/"; + } - // cache & return - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); - return info; - }); + // add result + result.Errors = errors.ToArray(); + mods[mod.ID] = result; } - return result; + return mods; } @@ -158,5 +178,63 @@ namespace StardewModdingAPI.Web.Controllers modID = parts[1].Trim(); return true; } + + /// Get the mods for which the API should return data. + /// The search model. + private IEnumerable GetSearchMods(ModSearchModel model) + { + if (model == null) + yield break; + + // yield standard entries + if (model.Mods != null) + { + foreach (ModSearchEntryModel mod in model.Mods) + yield return mod; + } + + // yield mod update keys if backwards compatible + if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + { + foreach (string updateKey in model.ModKeys.Distinct()) + yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); + } + } + + /// Get the mod info for an update key. + /// The namespaced update key. + private async Task GetInfoForUpdateKeyAsync(string updateKey) + { + // parse update key + if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) + return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + + // fetch mod info + return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + ModInfoModel result = await repository.GetModInfoAsync(modID); + if (result.Error != null) + { + if (result.Version == null) + result.Error = $"The update key '{updateKey}' matches a mod with no version number."; + else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; + } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); + return result; + }); + } + + /// Get whether the API should return data in a backwards compatible way. + /// The last version for which data should be backwards compatible. + private bool ShouldBeBackwardsCompatible(string maxVersion) + { + string actualVersion = (string)this.RouteData.Values["version"]; + return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs new file mode 100644 index 00000000..ccb0699c --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// Generic metadata about a mod. + public class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's latest release number. + public string Version { get; set; } + + /// The mod's latest optional release, if newer than . + public string PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) + { + this.Name = name; + this.Version = version; + this.PreviewVersion = previewVersion; + this.Url = url; + this.Error = error; + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b71c8056..d3ec0035 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework { @@ -46,11 +46,9 @@ namespace StardewModdingAPI.Framework /// Whether the mod is a content pack. bool IsContentPack { get; } - /// The update status of this mod (if any). - ModUpdateStatus UpdateStatus { get; } + /// The update-check metadata for this mod (if any). + ModEntryModel UpdateCheckData { get; } - /// The preview update status of this mod (if any). - ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -78,13 +76,9 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); - /// Set the update status. - /// The mod update status. - IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); - - /// Set the preview update status. - /// The mod preview update status. - IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); + /// Set the update-check metadata for this mod. + /// The update-check metadata. + IModMetadata SetUpdateData(ModEntryModel data); /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index deb12bdc..3801fac3 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -40,9 +40,12 @@ namespace StardewModdingAPI.Framework.ModData /// Get a semantic remote version for update checks. /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) { - return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 88d2770c..02a77778 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework.ModLoading { @@ -44,11 +44,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod-provided API (if any). public object Api { get; private set; } - /// The update status of this mod (if any). - public ModUpdateStatus UpdateStatus { get; private set; } - - /// The preview update status of this mod (if any). - public ModUpdateStatus PreviewUpdateStatus { get; private set; } + /// The update-check metadata for this mod (if any). + public ModEntryModel UpdateCheckData { get; private set; } /// Whether the mod is a content pack. public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -122,19 +119,11 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the update status. - /// The mod update status. - public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) - { - this.UpdateStatus = updateStatus; - return this; - } - - /// Set the preview update status. - /// The mod preview update status. - public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) + /// Set the update-check metadata for this mod. + /// The update-check metadata. + public IModMetadata SetUpdateData(ModEntryModel data) { - this.PreviewUpdateStatus = previewUpdateStatus; + this.UpdateCheckData = data; return this; } diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs deleted file mode 100644 index efb32aef..00000000 --- a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace StardewModdingAPI.Framework.ModUpdateChecking -{ - /// Update status for a mod. - internal class ModUpdateStatus - { - /********* - ** Accessors - *********/ - /// The version that this mod can be updated to (if any). - public ISemanticVersion Version { get; } - - /// The error checking for updates of this mod (if any). - public string Error { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The version that this mod can be update to. - public ModUpdateStatus(ISemanticVersion version) - { - this.Version = version; - } - - /// Construct an instance. - /// The error checking for updates of this mod. - public ModUpdateStatus(string error) - { - this.Error = error; - } - - /// Construct an instance. - public ModUpdateStatus() - { - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2ee18a29..ccdf98ef 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -8,6 +8,7 @@ using System.Net; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Xna.Framework.Input; @@ -24,7 +25,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -592,14 +592,14 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; - if (response.Error != null) + if (latestStable == null && response.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {response.Error}"); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) { @@ -634,103 +634,72 @@ namespace StardewModdingAPI { HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - // prepare update keys - Dictionary modsByKey = - ( - from mod in mods - where - mod.Manifest?.UpdateKeys != null - && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // fetch results - this.Monitor.Log($" Checking {modsByKey.Count} mod update keys.", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary> updatesByMod = new Dictionary>(); - foreach (var result in results) + // prepare search model + List searchMods = new List(); + foreach (IModMetadata mod in mods) { - IModMetadata mod = result.Mod; - ModInfoModel remoteInfo = result.Info; - - // handle error - if (remoteInfo.Error != null) - { - if (mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - if (mod.PreviewUpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); + if (!mod.HasManifest()) continue; - } - // normalise versions - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); - bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + } - if (!validVersion && mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); - if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) - mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary results = client.GetModInfo(searchMods.ToArray()); - if (!validVersion && !validPreviewVersion) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); + // extract update alerts & errors + var updates = new List>(); + var errors = new StringBuilder(); + foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) + { + // link to update-check data + if (!mod.HasManifest() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) continue; - } - - // compare versions - bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); - bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; + mod.SetUpdateData(result); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); - if (isUpdate) + // handle errors + if (result.Errors != null && result.Errors.Any()) { - if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) - { - updatesByMod[mod] = new Tuple(remoteInfo, isPreviewUpdate); - - if (isPreviewUpdate) - mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); - else - mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); - } + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName} update error: {result.Errors[0]}" + : $" {mod.DisplayName} update errors:\n - {string.Join("\n - ", result.Errors)}" + ); } - } - // set mods to have no updates - foreach (IModMetadata mod in results.Select(item => item.Mod) - .Where(item => !updatesByMod.ContainsKey(item))) - { - mod.SetUpdateStatus(new ModUpdateStatus()); - mod.SetPreviewUpdateStatus(new ModUpdateStatus()); + // parse versions + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + ISemanticVersion latestVersion = result.Version != null + ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) + : null; + ISemanticVersion optionalVersion = result.PreviewVersion != null + ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) + : null; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) + updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); } - // output - if (updatesByMod.Any()) + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) { this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {(entry.Value.Item2 ? entry.Value.Item1.PreviewVersion : entry.Value.Item1.Version)}: {entry.Value.Item1.Url}", LogLevel.Alert); + this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updates) + { + IModMetadata mod = entry.Item1; + ISemanticVersion newVersion = entry.Item2; + string newUrl = entry.Item3; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } } else this.Monitor.Log(" All mods up to date.", LogLevel.Trace); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 916dd053..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,7 +110,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs new file mode 100644 index 00000000..0f268231 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a mod. + public class ModEntryModel + { + /********* + ** Accessors + *********/ + /// The mod's unique ID (if known). + public string ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The mod's latest release number. + public string Version { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The mod's latest optional release, if newer than . + public string PreviewVersion { get; set; } + + /// The web URL to the mod's latest optional release, if newer than . + public string PreviewUrl { get; set; } + + /// The errors that occurred while fetching update data. + public string[] Errors { get; set; } = new string[0]; + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs deleted file mode 100644 index c8e296f0..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Generic metadata about a mod. - public class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's latest release number. - public string Version { get; set; } - - /// The mod's latest optional release, if newer than . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) - { - this.Name = name; - this.Version = version; - this.PreviewVersion = previewVersion; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index c0ee34ea..ffca32ca 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi @@ -10,10 +10,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// The namespaced mod keys to search. + [Obsolete] public string[] ModKeys { get; set; } - /// Whether to allow non-semantic versions, instead of returning an error for those. - public bool AllowInvalidVersions { get; set; } + /// The mods for which to find data. + public ModSearchEntryModel[] Mods { get; set; } /********* @@ -26,12 +27,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi } /// Construct an instance. - /// The namespaced mod keys to search. - /// Whether to allow non-semantic versions, instead of returning an error for those. - public ModSearchModel(IEnumerable modKeys, bool allowInvalidVersions) + /// The mods to search. + public ModSearchModel(ModSearchEntryModel[] mods) { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; + this.Mods = mods.ToArray(); } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs new file mode 100644 index 00000000..bca47647 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies the identifiers for a mod to match. + public class ModSearchEntryModel + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The namespaced mod update keys (if available). + public string[] UpdateKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchEntryModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The unique mod ID. + /// The namespaced mod update keys (if available). + public ModSearchEntryModel(string id, string[] updateKeys) + { + this.ID = id; + this.UpdateKeys = updateKeys ?? new string[0]; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index d94b0259..892dfeba 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using Newtonsoft.Json; @@ -31,44 +30,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Version = version; } - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) + /// Get metadata about a set of mods from the web API. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params ModSearchEntryModel[] mods) { - return this.Post>( + return this.Post>( $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) + new ModSearchModel(mods) ); } - /// Get the latest version for a mod. - /// The update keys to search. - public ISemanticVersion GetLatestVersion(string[] updateKeys) - { - if (!updateKeys.Any()) - return null; - - // fetch update results - ModInfoModel[] results = this - .GetModInfo(updateKeys) - .Values - .Where(p => p.Error == null) - .ToArray(); - if (!results.Any()) - return null; - - ISemanticVersion latest = null; - foreach (ModInfoModel result in results) - { - if (!SemanticVersion.TryParse(result.PreviewVersion ?? result.Version, out ISemanticVersion cur)) - continue; - - if (latest == null || cur.IsNewerThan(latest)) - latest = cur; - } - return latest; - } - /********* ** Private methods -- cgit From af92f2dc13c9a259c25a15d9bdfb80d73a0bec83 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 15:08:58 -0400 Subject: add more verbose logs to simplify troubleshooting --- src/SMAPI/Framework/SGame.cs | 18 +++++++++++++----- .../Framework/Clients/WebApi/ModEntryModel.cs | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 984c1f57..abe7bbc5 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -382,7 +382,7 @@ namespace StardewModdingAPI.Framework if (this.Watchers.WindowSizeWatcher.IsChanged) { if (this.VerboseLogging) - this.Monitor.Log($"Context: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); this.Events.Graphics_Resize.Raise(); this.Watchers.WindowSizeWatcher.Reset(); } @@ -415,6 +415,8 @@ namespace StardewModdingAPI.Framework int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; this.Watchers.MouseWheelScrollWatcher.Reset(); + if (this.VerboseLogging) + this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now)); } @@ -426,6 +428,9 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { + if (this.VerboseLogging) + this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState)); this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); @@ -445,6 +450,9 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { + if (this.VerboseLogging) + this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState)); this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); @@ -605,7 +613,7 @@ namespace StardewModdingAPI.Framework this.Watchers.TimeWatcher.Reset(); if (this.VerboseLogging) - this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace); + this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); } @@ -629,7 +637,7 @@ namespace StardewModdingAPI.Framework foreach (KeyValuePair> pair in curPlayer.GetChangedSkills()) { if (this.VerboseLogging) - this.Monitor.Log($"Context: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(pair.Key, pair.Value.CurrentValue)); } @@ -638,7 +646,7 @@ namespace StardewModdingAPI.Framework if (changedItems.Any()) { if (this.VerboseLogging) - this.Monitor.Log("Context: player inventory changed.", LogLevel.Trace); + this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList())); } @@ -1113,7 +1121,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } - label_140: + label_140: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 0f268231..e4ab168e 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod name. public string Name { get; set; } - /// The mod's latest release number. + /// The mod's latest version number. public string Version { get; set; } /// The mod's web URL. -- cgit From 9d33aaf832652a2183af863db6103bf6f8e5a682 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 18:53:33 -0400 Subject: update web/toolkit packages --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 14 +++++++------- .../StardewModdingAPI.Toolkit.csproj | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 9210565a..a409e6eb 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 6688a2a1..dda7c17c 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -10,7 +10,7 @@ - + -- cgit From 71efadf2322a622bc5a74614b1575d2680a84165 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:26:42 -0400 Subject: add project for toolkit interfaces visible to SMAPI mods (#532) --- build/common.targets | 8 +- build/prepare-install-package.targets | 4 + build/prepare-nuget-package.targets | 2 + src/SMAPI.Installer/InteractiveInstaller.cs | 2 + src/SMAPI.ModBuildConfig/build/smapi.targets | 5 ++ src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 4 + src/SMAPI.sln | 89 ++++++++++++---------- src/SMAPI/StardewModdingAPI.csproj | 4 + .../Properties/AssemblyInfo.cs | 4 + ...StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 19 +++++ .../Properties/AssemblyInfo.cs | 3 - .../StardewModdingAPI.Toolkit.csproj | 8 ++ 12 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/build/common.targets b/build/common.targets index 0b8f278f..5b6511f8 100644 --- a/build/common.targets +++ b/build/common.targets @@ -25,7 +25,7 @@ - + @@ -113,10 +113,14 @@ - + + + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 5e00d663..33a92f71 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,6 +41,8 @@ + + @@ -56,6 +58,8 @@ + + diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 5dbc5508..11d1845e 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -13,6 +13,8 @@ + + diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index f9239604..1221f659 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -96,6 +96,8 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.metadata.json"); yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); + yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d33a9637..0869be66 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -92,6 +92,11 @@ false true + + $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll + false + true + $(GamePath)\xTile.dll false diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 9a203b3b..b2d98d23 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -76,6 +76,10 @@ {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} StardewModdingAPI.Toolkit diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 0eb42cce..005537de 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.Save EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit.CoreInterfaces", "StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj", "{D5CFD923-37F1-4BC3-9BE8-E506E202AC28}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 @@ -70,50 +72,53 @@ Global SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Default = Debug|Default - Release|Default = Release|Default + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Default.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Default.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Default.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Default.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Default.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Default.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Default.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Default.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Default.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Default.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Default.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Default.Build.0 = Release|x86 - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Default.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Default.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Default.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Default.Build.0 = Release|Any CPU - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.ActiveCfg = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Default.Build.0 = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.ActiveCfg = Release|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Default.Build.0 = Release|x86 - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.ActiveCfg = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Default.Build.0 = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.ActiveCfg = Release|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Default.Build.0 = Release|Any CPU + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.Build.0 = Release|x86 + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.Build.0 = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.ActiveCfg = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.Build.0 = Debug|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.ActiveCfg = Release|x86 + {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.Build.0 = Release|x86 + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.Build.0 = Release|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2e3ba1cd..a429d204 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -336,6 +336,10 @@ + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} StardewModdingAPI.Toolkit diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a29ba6cf --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("SMAPI.Toolkit.CoreInterfaces")] +[assembly: AssemblyDescription("Provides toolkit interfaces which are available to SMAPI mods.")] diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj new file mode 100644 index 00000000..e003122e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -0,0 +1,19 @@ + + + + net4.5;netstandard2.0 + false + + + + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces + StardewModdingAPI + + + + + + + + + diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs index 22dcdd96..1bb19e8c 100644 --- a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs @@ -3,8 +3,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] -[assembly: AssemblyProduct("SMAPI Toolkit")] -[assembly: AssemblyVersion("0.1.0")] -[assembly: AssemblyFileVersion("0.1.0")] [assembly: InternalsVisibleTo("StardewModdingAPI")] [assembly: InternalsVisibleTo("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index dda7c17c..904f6786 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -9,12 +9,20 @@ ..\..\bin\$(Configuration)\SMAPI.Toolkit + + + + + + + + -- cgit From 316242eeb2b6b6e711ab98f64c147a59c1d0aab8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:29:10 -0400 Subject: merge ISemanticVersion interfaces into new project (#532) --- .../StardewModdingAPI.Mods.SaveBackup.csproj | 4 + src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI/Constants.cs | 16 +--- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 11 ++- src/SMAPI/ISemanticVersion.cs | 62 -------------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 + src/SMAPI/Program.cs | 2 +- src/SMAPI/SemanticVersion.cs | 31 ++++--- src/SMAPI/StardewModdingAPI.csproj | 1 - .../ISemanticVersion.cs | 62 ++++++++++++++ src/StardewModdingAPI.Toolkit/ISemanticVersion.cs | 46 ----------- src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 95 ++++++++++++++-------- 12 files changed, 158 insertions(+), 177 deletions(-) delete mode 100644 src/SMAPI/ISemanticVersion.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs delete mode 100644 src/StardewModdingAPI.Toolkit/ISemanticVersion.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index 44fff536..afba15a1 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -51,6 +51,10 @@ StardewModdingAPI False + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index f4ade7de..09bad112 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; - bool isBeta = version.Tag != null; + bool isBeta = version.IsPrerelease(); bool isForDevs = match.Groups["forDevs"].Success; yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 65ca7866..df70d603 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6-beta.17"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.21"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -70,9 +70,6 @@ namespace StardewModdingAPI /**** ** Internal ****/ - /// SMAPI's current semantic version as a mod toolkit version. - internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } - /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; @@ -113,15 +110,6 @@ namespace StardewModdingAPI /********* ** Internal methods *********/ - /// Initialise the static values. - static Constants() - { - Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.17"); - Constants.MinimumGameVersion = new GameVersion("1.3.20"); - - Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); - } - /// Get metadata for mapping assemblies to the current platform. /// The target game platform. internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 5c7db902..de9c439a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The new type to reference. private readonly Type ToType; + /// A lambda which indicates whether a matching type reference should be rewritten. + private readonly Func ShouldRewrite; + /********* ** Public methods @@ -24,11 +27,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - public TypeReferenceRewriter(string fromTypeFullName, Type toType) + /// A lambda which indicates whether a matching type reference should be rewritten. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldRewrite = null) : base(fromTypeFullName, InstructionHandleResult.None) { this.FromTypeName = fromTypeFullName; this.ToType = toType; + this.ShouldRewrite = shouldRewrite ?? (type => true); } /// Perform the predefined logic for a method if applicable. @@ -135,7 +140,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // root type if (type.FullName == this.FromTypeName) + { + if (!this.ShouldRewrite(type)) + return type; return module.ImportReference(this.ToType); + } // generic arguments if (type is GenericInstanceType genericType) diff --git a/src/SMAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs deleted file mode 100644 index 961ef777..00000000 --- a/src/SMAPI/ISemanticVersion.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - int PatchVersion { get; } - - /// An optional build tag. - string Build { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsOlderThan(string other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsNewerThan(string other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - bool IsBetween(string min, string max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 1063ae71..543d44a7 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), + // rewrite for SMAPI 2.6 + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // moved to SMAPI.Toolkit.CoreInterfaces + // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7b5176a0..a51d6380 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -585,7 +585,7 @@ namespace StardewModdingAPI #if !SMAPI_FOR_WINDOWS url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac #endif - WebApiClient client = new WebApiClient(url, Constants.ApiVersionForToolkit); + WebApiClient client = new WebApiClient(url, Constants.ApiVersion); this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index c4dd1912..587ff286 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -10,23 +10,23 @@ namespace StardewModdingAPI ** Properties *********/ /// The underlying semantic version implementation. - private readonly Toolkit.ISemanticVersion Version; + private readonly ISemanticVersion Version; /********* ** Accessors *********/ /// The major version incremented for major API changes. - public int MajorVersion => this.Version.Major; + public int MajorVersion => this.Version.MajorVersion; /// The minor version incremented for backwards-compatible changes. - public int MinorVersion => this.Version.Minor; + public int MinorVersion => this.Version.MinorVersion; /// The patch version for backwards-compatible bug fixes. - public int PatchVersion => this.Version.Patch; + public int PatchVersion => this.Version.PatchVersion; /// An optional build tag. - public string Build => this.Version.Tag; + public string Build => this.Version.Build; /********* @@ -56,7 +56,7 @@ namespace StardewModdingAPI /// Construct an instance. /// The underlying semantic version implementation. - internal SemanticVersion(Toolkit.ISemanticVersion version) + internal SemanticVersion(ISemanticVersion version) { this.Version = version; } @@ -64,7 +64,7 @@ namespace StardewModdingAPI /// Whether this is a pre-release version. public bool IsPrerelease() { - return !string.IsNullOrWhiteSpace(this.Build); + return this.Version.IsPrerelease(); } /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. @@ -73,15 +73,14 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - Toolkit.ISemanticVersion toolkitOther = new Toolkit.SemanticVersion(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); - return this.Version.CompareTo(toolkitOther); + return this.Version.CompareTo(other); } /// Get whether this version is older than the specified version. /// The version to compare with this instance. public bool IsOlderThan(ISemanticVersion other) { - return this.CompareTo(other) < 0; + return this.Version.IsOlderThan(other); } /// Get whether this version is older than the specified version. @@ -89,14 +88,14 @@ namespace StardewModdingAPI /// The specified version is not a valid semantic version. public bool IsOlderThan(string other) { - return this.IsOlderThan(new SemanticVersion(other)); + return this.Version.IsOlderThan(other); } /// Get whether this version is newer than the specified version. /// The version to compare with this instance. public bool IsNewerThan(ISemanticVersion other) { - return this.CompareTo(other) > 0; + return this.Version.IsNewerThan(other); } /// Get whether this version is newer than the specified version. @@ -104,7 +103,7 @@ namespace StardewModdingAPI /// The specified version is not a valid semantic version. public bool IsNewerThan(string other) { - return this.IsNewerThan(new SemanticVersion(other)); + return this.Version.IsNewerThan(other); } /// Get whether this version is between two specified versions (inclusively). @@ -112,7 +111,7 @@ namespace StardewModdingAPI /// The maximum version. public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { - return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + return this.Version.IsBetween(min, max); } /// Get whether this version is between two specified versions (inclusively). @@ -121,7 +120,7 @@ namespace StardewModdingAPI /// One of the specified versions is not a valid semantic version. public bool IsBetween(string min, string max) { - return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + return this.Version.IsBetween(min, max); } /// Indicates whether the current object is equal to another object of the same type. @@ -144,7 +143,7 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. internal static bool TryParse(string version, out ISemanticVersion parsed) { - if (Toolkit.SemanticVersion.TryParse(version, out Toolkit.ISemanticVersion versionImpl)) + if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl)) { parsed = new SemanticVersion(versionImpl); return true; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index a429d204..c872391c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -280,7 +280,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs new file mode 100644 index 00000000..961ef777 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -0,0 +1,62 @@ +using System; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + int PatchVersion { get; } + + /// An optional build tag. + string Build { get; } + + + /********* + ** Accessors + *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsOlderThan(string other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsNewerThan(string other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + bool IsBetween(string min, string max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs deleted file mode 100644 index ca62d393..00000000 --- a/src/StardewModdingAPI.Toolkit/ISemanticVersion.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int Major { get; } - - /// The minor version incremented for backwards-compatible changes. - int Minor { get; } - - /// The patch version for backwards-compatible bug fixes. - int Patch { get; } - - /// An optional prerelease tag. - string Tag { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index de480416..156d58ce 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -31,16 +31,16 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// The major version incremented for major API changes. - public int Major { get; } + public int MajorVersion { get; } /// The minor version incremented for backwards-compatible changes. - public int Minor { get; } + public int MinorVersion { get; } /// The patch version for backwards-compatible bug fixes. - public int Patch { get; } + public int PatchVersion { get; } /// An optional prerelease tag. - public string Tag { get; } + public string Build { get; } /********* @@ -53,10 +53,10 @@ namespace StardewModdingAPI.Toolkit /// An optional prerelease tag. public SemanticVersion(int major, int minor, int patch, string tag = null) { - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.Tag = this.GetNormalisedTag(tag); + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.Build = this.GetNormalisedTag(tag); this.AssertValid(); } @@ -69,9 +69,9 @@ namespace StardewModdingAPI.Toolkit if (version == null) throw new ArgumentNullException(nameof(version), "The input version can't be null."); - this.Major = version.Major; - this.Minor = version.Minor; - this.Patch = version.Build; + this.MajorVersion = version.Major; + this.MinorVersion = version.Minor; + this.PatchVersion = version.Build; this.AssertValid(); } @@ -90,10 +90,10 @@ namespace StardewModdingAPI.Toolkit throw new FormatException($"The input '{version}' isn't a valid semantic version."); // initialise - this.Major = int.Parse(match.Groups["major"].Value); - this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + this.MajorVersion = int.Parse(match.Groups["major"].Value); + this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; this.AssertValid(); } @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Toolkit { if (other == null) throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); } /// Indicates whether the current object is equal to another object of the same type. @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Toolkit /// Whether this is a pre-release version. public bool IsPrerelease() { - return !string.IsNullOrWhiteSpace(this.Tag); + return !string.IsNullOrWhiteSpace(this.Build); } /// Get whether this version is older than the specified version. @@ -129,6 +129,14 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other) < 0; } + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsOlderThan(string other) + { + return this.IsOlderThan(new SemanticVersion(other)); + } + /// Get whether this version is newer than the specified version. /// The version to compare with this instance. public bool IsNewerThan(ISemanticVersion other) @@ -136,6 +144,14 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other) > 0; } + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsNewerThan(string other) + { + return this.IsNewerThan(new SemanticVersion(other)); + } + /// Get whether this version is between two specified versions (inclusively). /// The minimum version. /// The maximum version. @@ -144,16 +160,25 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; } + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + public bool IsBetween(string min, string max) + { + return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + } + /// Get a string representation of the version. public override string ToString() { // version - string result = this.Patch != 0 - ? $"{this.Major}.{this.Minor}.{this.Patch}" - : $"{this.Major}.{this.Minor}"; + string result = this.PatchVersion != 0 + ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" + : $"{this.MajorVersion}.{this.MinorVersion}"; // tag - string tag = this.Tag; + string tag = this.Build; if (tag != null) result += $"-{tag}"; return result; @@ -201,17 +226,17 @@ namespace StardewModdingAPI.Toolkit const int curOlder = -1; // compare stable versions - if (this.Major != otherMajor) - return this.Major.CompareTo(otherMajor); - if (this.Minor != otherMinor) - return this.Minor.CompareTo(otherMinor); - if (this.Patch != otherPatch) - return this.Patch.CompareTo(otherPatch); - if (this.Tag == otherTag) + if (this.MajorVersion != otherMajor) + return this.MajorVersion.CompareTo(otherMajor); + if (this.MinorVersion != otherMinor) + return this.MinorVersion.CompareTo(otherMinor); + if (this.PatchVersion != otherPatch) + return this.PatchVersion.CompareTo(otherPatch); + if (this.Build == otherTag) return same; // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool curIsStable = string.IsNullOrWhiteSpace(this.Build); bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); if (curIsStable) return curNewer; @@ -219,7 +244,7 @@ namespace StardewModdingAPI.Toolkit return curOlder; // compare two pre-release tag values - string[] curParts = this.Tag.Split('.', '-'); + string[] curParts = this.Build.Split('.', '-'); string[] otherParts = otherTag.Split('.', '-'); for (int i = 0; i < curParts.Length; i++) { @@ -248,15 +273,15 @@ namespace StardewModdingAPI.Toolkit /// Assert that the current version is valid. private void AssertValid() { - if (this.Major < 0 || this.Minor < 0 || this.Patch < 0) + if (this.MajorVersion < 0 || this.MinorVersion < 0 || this.PatchVersion < 0) throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); - if (this.Major == 0 && this.Minor == 0 && this.Patch == 0) + if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0) throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); - if (this.Tag != null) + if (this.Build != null) { - if (this.Tag.Trim() == "") + if (this.Build.Trim() == "") throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); - if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) + if (!Regex.IsMatch(this.Build, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); } } -- cgit From b08e27d13a1f0c82656df95212fc40588b3b5314 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:51:51 -0400 Subject: merge IManifest interfaces into new project (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 25 +++--- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 9 +- src/SMAPI/Framework/Models/Manifest.cs | 97 ---------------------- .../Framework/Models/ManifestContentPackFor.cs | 36 -------- src/SMAPI/Framework/Models/ManifestDependency.cs | 40 --------- .../Serialisation/SemanticVersionConverter.cs | 40 --------- src/SMAPI/IManifest.cs | 44 ---------- src/SMAPI/IManifestContentPackFor.cs | 12 --- src/SMAPI/IManifestDependency.cs | 18 ---- src/SMAPI/Metadata/InstructionMetadata.cs | 7 +- src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 7 -- .../IManifest.cs | 44 ++++++++++ .../IManifestContentPackFor.cs | 12 +++ .../IManifestDependency.cs | 18 ++++ .../Converters/SemanticVersionConverter.cs | 1 - .../Serialisation/Models/Manifest.cs | 37 +++++++-- .../Serialisation/Models/ManifestContentPackFor.cs | 8 +- .../Serialisation/Models/ManifestDependency.cs | 8 +- 20 files changed, 142 insertions(+), 328 deletions(-) delete mode 100644 src/SMAPI/Framework/Models/Manifest.cs delete mode 100644 src/SMAPI/Framework/Models/ManifestContentPackFor.cs delete mode 100644 src/SMAPI/Framework/Models/ManifestDependency.cs delete mode 100644 src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs delete mode 100644 src/SMAPI/IManifest.cs delete mode 100644 src/SMAPI/IManifestContentPackFor.cs delete mode 100644 src/SMAPI/IManifestDependency.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs create mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 2fbeb9b6..a0fe2023 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -7,9 +7,9 @@ using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Core { @@ -472,17 +472,18 @@ namespace StardewModdingAPI.Tests.Core /// The value. private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) { - return new Manifest( - uniqueID: id ?? $"{Sample.String()}.{Sample.String()}", - name: name ?? id ?? Sample.String(), - author: Sample.String(), - description: Sample.String(), - version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - entryDll: entryDll ?? $"{Sample.String()}.dll", - contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID) : null, - minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - dependencies: dependencies - ); + return new Manifest + { + UniqueID = id ?? $"{Sample.String()}.{Sample.String()}", + Name = name ?? id ?? Sample.String(), + Author = Sample.String(), + Description = Sample.String(), + Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + EntryDll = entryDll ?? $"{Sample.String()}.dll", + ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, + MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, + Dependencies = dependencies + }; } /// Get a randomised basic manifest. diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 18904857..d9498e83 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers @@ -185,7 +185,7 @@ namespace StardewModdingAPI.Framework.ModHelpers author: author, description: description, version: version, - contentPackFor: new ManifestContentPackFor(this.ModID) + contentPackFor: this.ModID ); // create content pack diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 3366e8c1..fde921e6 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -4,10 +4,9 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; -using ToolkitManifest = StardewModdingAPI.Toolkit.Serialisation.Models.Manifest; namespace StardewModdingAPI.Framework.ModLoading { @@ -33,15 +32,13 @@ namespace StardewModdingAPI.Framework.ModLoading string path = Path.Combine(modDir.FullName, "manifest.json"); try { - ToolkitManifest rawManifest = jsonHelper.ReadJsonFile(path); - if (rawManifest == null) + manifest = jsonHelper.ReadJsonFile(path); + if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else - manifest = new Manifest(rawManifest); } catch (SParseException ex) { diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs deleted file mode 100644 index 92ffe0dc..00000000 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// A manifest which describes a mod for SMAPI. - internal class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; } - - /// A brief description of the mod. - public string Description { get; } - - /// The mod author's name. - public string Author { get; } - - /// The mod version. - public ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; } - - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - public string EntryDll { get; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - public IManifestContentPackFor ContentPackFor { get; } - - /// The other mods that must be loaded before this mod. - public IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - public string[] UpdateKeys { get; set; } - - /// The unique mod ID. - public string UniqueID { get; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit manifest. - public Manifest(Toolkit.Serialisation.Models.Manifest manifest) - : this( - uniqueID: manifest.UniqueID, - name: manifest.Name, - author: manifest.Author, - description: manifest.Description, - version: manifest.Version != null ? new SemanticVersion(manifest.Version) : null, - entryDll: manifest.EntryDll, - minimumApiVersion: manifest.MinimumApiVersion != null ? new SemanticVersion(manifest.MinimumApiVersion) : null, - contentPackFor: manifest.ContentPackFor != null ? new ManifestContentPackFor(manifest.ContentPackFor) : null, - dependencies: manifest.Dependencies?.Select(p => p != null ? (IManifestDependency)new ManifestDependency(p) : null).ToArray(), - updateKeys: manifest.UpdateKeys, - extraFields: manifest.ExtraFields - ) - { } - - /// Construct an instance for a transitional content pack. - /// The unique mod ID. - /// The mod name. - /// The mod author's name. - /// A brief description of the mod. - /// The mod version. - /// The name of the DLL in the directory that has the method. Mutually exclusive with . - /// The minimum SMAPI version required by this mod, if any. - /// The modID which will read this as a content pack. Mutually exclusive with . - /// The other mods that must be loaded before this mod. - /// The namespaced mod IDs to query for updates (like Nexus:541). - /// Any manifest fields which didn't match a valid field. - public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string entryDll = null, ISemanticVersion minimumApiVersion = null, IManifestContentPackFor contentPackFor = null, IManifestDependency[] dependencies = null, string[] updateKeys = null, IDictionary extraFields = null) - { - this.Name = name; - this.Author = author; - this.Description = description; - this.Version = version; - this.UniqueID = uniqueID; - this.UpdateKeys = new string[0]; - this.EntryDll = entryDll; - this.ContentPackFor = contentPackFor; - this.MinimumApiVersion = minimumApiVersion; - this.Dependencies = dependencies ?? new IManifestDependency[0]; - this.UpdateKeys = updateKeys ?? new string[0]; - this.ExtraFields = extraFields; - } - } -} diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs deleted file mode 100644 index 90e20c6a..00000000 --- a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs +++ /dev/null @@ -1,36 +0,0 @@ -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; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit instance. - public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor) - { - this.UniqueID = contentPackFor.UniqueID; - this.MinimumVersion = contentPackFor.MinimumVersion != null ? new SemanticVersion(contentPackFor.MinimumVersion) : null; - } - - /// Construct an instance. - /// The unique ID of the mod which can read this content pack. - /// The minimum required version (if any). - public ManifestContentPackFor(string uniqueID, ISemanticVersion minimumVersion = null) - { - this.UniqueID = uniqueID; - this.MinimumVersion = minimumVersion; - } - } -} diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs deleted file mode 100644 index e92597f3..00000000 --- a/src/SMAPI/Framework/Models/ManifestDependency.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A mod dependency listed in a mod manifest. - internal class ManifestDependency : IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - public string UniqueID { get; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The toolkit instance. - public ManifestDependency(Toolkit.Serialisation.Models.ManifestDependency dependency) - : this(dependency.UniqueID, dependency.MinimumVersion?.ToString(), dependency.IsRequired) { } - - /// Construct an instance. - /// The unique mod ID to require. - /// The minimum required version (if any). - /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) - { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; - this.IsRequired = required; - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs deleted file mode 100644 index 3e05a440..00000000 --- a/src/SMAPI/Framework/Serialisation/SemanticVersionConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) - { - int major = obj.ValueIgnoreCase("MajorVersion"); - int minor = obj.ValueIgnoreCase("MinorVersion"); - int patch = obj.ValueIgnoreCase("PatchVersion"); - string build = obj.ValueIgnoreCase("Build"); - if (build == "0") - build = null; // '0' from incorrect examples in old SMAPI documentation - - return new SemanticVersion(major, minor, patch, build); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) - { - 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 (path: {path})."); - return version; - } - } -} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs deleted file mode 100644 index 6c07d374..00000000 --- a/src/SMAPI/IManifest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// A manifest which describes a mod for SMAPI. - public interface IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - string Name { get; } - - /// A brief description of the mod. - string Description { get; } - - /// The mod author's name. - string Author { get; } - - /// The mod version. - ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - ISemanticVersion MinimumApiVersion { get; } - - /// The unique mod ID. - string UniqueID { get; } - - /// 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; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; } - - /// Any manifest fields which didn't match a valid field. - IDictionary ExtraFields { get; } - } -} diff --git a/src/SMAPI/IManifestContentPackFor.cs b/src/SMAPI/IManifestContentPackFor.cs deleted file mode 100644 index f05a3873..00000000 --- a/src/SMAPI/IManifestContentPackFor.cs +++ /dev/null @@ -1,12 +0,0 @@ -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/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs deleted file mode 100644 index e86cd1f4..00000000 --- a/src/SMAPI/IManifestDependency.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI -{ - /// A mod dependency listed in a mod manifest. - public interface IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - bool IsRequired { get; } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 543d44a7..c5128eb1 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -37,8 +37,11 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 2.0 new VirtualEntryCallRemover(), - // rewrite for SMAPI 2.6 - new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // moved to SMAPI.Toolkit.CoreInterfaces + // rewrite for SMAPI 2.6 (types moved into SMAPI.Toolkit.CoreInterfaces) + new TypeReferenceRewriter("StardewModdingAPI.IManifest", typeof(IManifest), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestContentPackFor", typeof(IManifestContentPackFor), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.IManifestDependency", typeof(IManifestDependency), type => type.Scope.Name == "StardewModdingAPI"), + new TypeReferenceRewriter("StardewModdingAPI.ISemanticVersion", typeof(ISemanticVersion), type => type.Scope.Name == "StardewModdingAPI"), // rewrite for Stardew Valley 1.3 new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize), diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a51d6380..1b276988 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -201,8 +201,7 @@ namespace StardewModdingAPI new StringEnumConverter(), new ColorConverter(), new PointConverter(), - new RectangleConverter(), - new Framework.Serialisation.SemanticVersionConverter() + new RectangleConverter() }; foreach (JsonConverter converter in converters) this.JsonHelper.JsonSettings.Converters.Add(converter); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c872391c..4852f70c 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -126,11 +126,7 @@ - - - - @@ -186,7 +182,6 @@ - @@ -258,7 +253,6 @@ - @@ -275,7 +269,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs new file mode 100644 index 00000000..6c07d374 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// A manifest which describes a mod for SMAPI. + public interface IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + string Name { get; } + + /// A brief description of the mod. + string Description { get; } + + /// The mod author's name. + string Author { get; } + + /// The mod version. + ISemanticVersion Version { get; } + + /// The minimum SMAPI version required by this mod, if any. + ISemanticVersion MinimumApiVersion { get; } + + /// The unique mod ID. + string UniqueID { get; } + + /// 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; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; } + + /// Any manifest fields which didn't match a valid field. + IDictionary ExtraFields { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/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/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs new file mode 100644 index 00000000..e86cd1f4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI +{ + /// A mod dependency listed in a mod manifest. + public interface IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 2ddaa1bf..eff95d1f 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs index 68987dd1..6ec57258 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Serialisation.Converters; namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A manifest which describes a mod for SMAPI. - public class Manifest + public class Manifest : IManifest { /********* ** Accessors @@ -20,21 +20,23 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string Author { get; set; } /// The mod version. - public SemanticVersion Version { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - public SemanticVersion MinimumApiVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the Entry 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 ManifestContentPackFor ContentPackFor { get; set; } + public IManifestContentPackFor ContentPackFor { get; set; } /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public ManifestDependency[] Dependencies { get; set; } + public IManifestDependency[] Dependencies { get; set; } /// The namespaced mod IDs to query for updates (like Nexus:541). public string[] UpdateKeys { get; set; } @@ -45,5 +47,30 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models /// Any manifest fields which didn't match a valid field. [JsonExtensionData] public IDictionary ExtraFields { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public Manifest() { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The modID which will read this as a content pack. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; + } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs index 00546533..64808dcf 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -1,7 +1,10 @@ +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// Indicates which mod can read the content pack represented by the containing manifest. - public class ManifestContentPackFor + public class ManifestContentPackFor : IManifestContentPackFor { /********* ** Accessors @@ -10,6 +13,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - public SemanticVersion MinimumVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumVersion { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs index d902f9ac..67e733dd 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -1,7 +1,10 @@ +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A mod dependency listed in a mod manifest. - public class ManifestDependency + public class ManifestDependency : IManifestDependency { /********* ** Accessors @@ -10,7 +13,8 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - public SemanticVersion MinimumVersion { get; set; } + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion MinimumVersion { get; set; } /// Whether the dependency must be installed to use the mod. public bool IsRequired { get; set; } -- cgit From 85efb3112912a28dbdc82b18d0be8dd117f8c8ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 23:01:39 -0400 Subject: fix ISemanticVersion deserialisation errors (#532) --- .../Serialisation/Converters/SemanticVersionConverter.cs | 8 ++++---- src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs | 2 -- .../Serialisation/Models/ManifestContentPackFor.cs | 4 ---- .../Serialisation/Models/ManifestDependency.cs | 4 ---- 4 files changed, 4 insertions(+), 14 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index eff95d1f..4f0949fa 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -2,8 +2,8 @@ using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { - /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter + /// Handles deserialisation of . + internal class SemanticVersionConverter : SimpleReadOnlyConverter { /********* ** Protected methods @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// Read a JSON object. /// The JSON object to read. /// The path to the current JSON node. - protected override SemanticVersion ReadObject(JObject obj, string path) + protected override ISemanticVersion ReadObject(JObject obj, string path) { int major = obj.ValueIgnoreCase("MajorVersion"); int minor = obj.ValueIgnoreCase("MinorVersion"); @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// Read a JSON string. /// The JSON string value. /// The path to the current JSON node. - protected override SemanticVersion ReadString(string str, string path) + protected override ISemanticVersion ReadString(string str, string path) { if (string.IsNullOrWhiteSpace(str)) return null; diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs index 6ec57258..6cb9496b 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -20,11 +20,9 @@ namespace StardewModdingAPI.Toolkit.Serialisation.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 Entry method. Mutually exclusive with . diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs index 64808dcf..d0e42216 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -1,6 +1,3 @@ -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// Indicates which mod can read the content pack represented by the containing manifest. @@ -13,7 +10,6 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumVersion { get; set; } } } diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs index 67e733dd..8db58d5d 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -1,6 +1,3 @@ -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - namespace StardewModdingAPI.Toolkit.Serialisation.Models { /// A mod dependency listed in a mod manifest. @@ -13,7 +10,6 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Models public string UniqueID { get; set; } /// The minimum required version (if any). - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumVersion { get; set; } /// Whether the dependency must be installed to use the mod. -- cgit From 5f19e4f2035c36f9c6c882da3767d6f29409db1c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:05:53 -0400 Subject: move mod DB parsing into toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- src/SMAPI/Constants.cs | 26 ---- 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 | 146 --------------------- src/SMAPI/Framework/ModData/ModDatabase.cs | 140 -------------------- 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 | 2 +- src/SMAPI/Framework/Models/SMetadata.cs | 15 --- src/SMAPI/Program.cs | 9 +- src/SMAPI/StardewModdingAPI.csproj | 7 - .../Framework/ModData/ModDataField.cs | 82 ++++++++++++ .../Framework/ModData/ModDataFieldKey.cs | 18 +++ .../Framework/ModData/ModDataRecord.cs | 146 +++++++++++++++++++++ .../Framework/ModData/ModDatabase.cs | 140 ++++++++++++++++++++ .../Framework/ModData/ModStatus.cs | 18 +++ .../Framework/ModData/ParsedModDataRecord.cs | 51 +++++++ .../Framework/ModData/SMetadata.cs | 14 ++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 39 ++++++ 22 files changed, 517 insertions(+), 511 deletions(-) delete mode 100644 src/SMAPI/Framework/ModData/ModDataField.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDataFieldKey.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDataRecord.cs delete mode 100644 src/SMAPI/Framework/ModData/ModDatabase.cs delete mode 100644 src/SMAPI/Framework/ModData/ModStatus.cs delete mode 100644 src/SMAPI/Framework/ModData/ParsedModDataRecord.cs delete mode 100644 src/SMAPI/Framework/Models/SMetadata.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index a0fe2023..e63057b3 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -6,8 +6,8 @@ using Moq; using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 01b99d62..c4a3813e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -22,14 +21,6 @@ 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}", - ["GitHub"] = "https://github.com/{0}/releases", - ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" - }; - /********* ** Accessors @@ -159,23 +150,6 @@ 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/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 6281c052..5a8689de 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ModData/ModDataField.cs b/src/SMAPI/Framework/ModData/ModDataField.cs deleted file mode 100644 index df906103..00000000 --- a/src/SMAPI/Framework/ModData/ModDataField.cs +++ /dev/null @@ -1,82 +0,0 @@ -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(p => !string.IsNullOrWhiteSpace(p)); - - // 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 deleted file mode 100644 index f68f575c..00000000 --- a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 56275f53..00000000 --- a/src/SMAPI/Framework/ModData/ModDataRecord.cs +++ /dev/null @@ -1,146 +0,0 @@ -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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) - { - // normalise version if possible - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) - version = parsed.ToString(); - - // fetch remote 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 deleted file mode 100644 index 62f37d9b..00000000 --- a/src/SMAPI/Framework/ModData/ModDatabase.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -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; - - /// 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(), key => null) { } - - /// Construct an instance. - /// The underlying mod data records indexed by default display name. - /// 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. - /// The manifest to match. - public ParsedModDataRecord GetParsed(IManifest manifest) - { - // get raw record - if (!this.TryGetRaw(manifest?.UniqueID, 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) - { - return this.TryGetRaw(id, out string displayName, out ModDataRecord _) - ? displayName - : 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 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) - { - if (!string.IsNullOrWhiteSpace(id)) - { - foreach (var entry in this.Records) - { - displayName = entry.Key; - record = entry.Value; - - // try main ID - if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - if (record.FormerIDs != null) - { - foreach (string part in record.FormerIDs.Split('|')) - { - if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - } - } - } - - displayName = null; - record = null; - return false; - } - } -} diff --git a/src/SMAPI/Framework/ModData/ModStatus.cs b/src/SMAPI/Framework/ModData/ModStatus.cs deleted file mode 100644 index 0e1d94d4..00000000 --- a/src/SMAPI/Framework/ModData/ModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 3801fac3..00000000 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ /dev/null @@ -1,51 +0,0 @@ -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 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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) - { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : null; - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 3a412009..4db25932 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index fde921e6..c1bc51ec 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; diff --git a/src/SMAPI/Framework/Models/SMetadata.cs b/src/SMAPI/Framework/Models/SMetadata.cs deleted file mode 100644 index 9ff495e9..00000000 --- a/src/SMAPI/Framework/Models/SMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using StardewModdingAPI.Framework.ModData; - -namespace StardewModdingAPI.Framework.Models -{ - /// The SMAPI predefined metadata. - internal class SMetadata - { - /******** - ** Accessors - ********/ - /// Extra metadata about mods. - public IDictionary ModData { get; set; } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 09d9969c..6f1fe761 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -21,7 +21,6 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; @@ -29,7 +28,9 @@ using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Converters; using StardewModdingAPI.Toolkit.Utilities; @@ -413,8 +414,8 @@ 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 - SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiMetadataPath)); - ModDatabase modDatabase = new ModDatabase(metadata.ModData, Constants.GetUpdateUrl); + ModToolkit toolkit = new ModToolkit(); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath, toolkit.GetUpdateUrl); // load mods { @@ -423,7 +424,7 @@ namespace StardewModdingAPI // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl); + resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4852f70c..f849ee53 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -132,11 +132,6 @@ - - - - - @@ -239,7 +234,6 @@ - @@ -263,7 +257,6 @@ - diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs new file mode 100644 index 00000000..b3954693 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A versioned mod metadata field. + public 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(p => !string.IsNullOrWhiteSpace(p)); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs new file mode 100644 index 00000000..09dd0cc5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.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/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs new file mode 100644 index 00000000..97ad0ca4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Raw mod metadata from SMAPI's internal mod list. + public 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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) + ? new SemanticVersion(newVersion) + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + // normalise version if possible + if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + version = parsed.ToString(); + + // fetch remote 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/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs new file mode 100644 index 00000000..c60d2bcb --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Handles access to SMAPI's internal mod metadata list. + public class ModDatabase + { + /********* + ** Properties + *********/ + /// 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(), key => null) { } + + /// Construct an instance. + /// The underlying mod data records indexed by default display name. + /// 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. + /// The manifest to match. + public ParsedModDataRecord GetParsed(IManifest manifest) + { + // get raw record + if (!this.TryGetRaw(manifest?.UniqueID, 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) + { + return this.TryGetRaw(id, out string displayName, out ModDataRecord _) + ? displayName + : 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 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) + { + if (!string.IsNullOrWhiteSpace(id)) + { + foreach (var entry in this.Records) + { + displayName = entry.Key; + record = entry.Value; + + // try main ID + if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (record.FormerIDs != null) + { + foreach (string part in record.FormerIDs.Split('|')) + { + if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + } + } + + displayName = null; + record = null; + return false; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs new file mode 100644 index 00000000..09da74bf --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Indicates how SMAPI should treat a mod. + public 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/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs new file mode 100644 index 00000000..74f11ea5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A parsed representation of the fields from a for a specific manifest. + public 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 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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + { + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs new file mode 100644 index 00000000..9553cca9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The SMAPI predefined metadata. + internal class SMetadata + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 6136186e..1723991e 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -1,5 +1,10 @@ +using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; +using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Toolkit { @@ -12,6 +17,14 @@ namespace StardewModdingAPI.Toolkit /// The default HTTP user agent for the toolkit. private readonly string UserAgent; + /// 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 readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["GitHub"] = "https://github.com/{0}/releases", + ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" + }; + /********* ** Public methods @@ -29,5 +42,31 @@ namespace StardewModdingAPI.Toolkit var client = new WikiCompatibilityClient(this.UserAgent); return await client.FetchAsync(); } + + /// Get SMAPI's internal mod database. + /// The file path for the SMAPI metadata file. + /// Get an update URL for an update key (if valid). + public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) + { + SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); + return new ModDatabase(metadata.ModData, getUpdateUrl); + } + + /// Get an update URL for an update key (if valid). + /// The update key. + internal 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 (this.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + return string.Format(urlTemplate, modID); + + return null; + } } } -- cgit From 82306a2c50f4d3df33d8ce62ca49628baf0cc3b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:40:31 -0400 Subject: encapsulate mod DB a bit better for use outside SMAPI (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 4 +- src/SMAPI/Framework/IModMetadata.cs | 2 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 +- .../Framework/ModData/MetadataModel.cs | 14 ++ .../Framework/ModData/ModDataModel.cs | 129 +++++++++++++++++ .../Framework/ModData/ModDataRecord.cs | 152 +++++++++------------ .../ModData/ModDataRecordVersionedFields.cs | 51 +++++++ .../Framework/ModData/ModDatabase.cs | 103 ++------------ .../Framework/ModData/ParsedModDataRecord.cs | 51 ------- .../Framework/ModData/SMetadata.cs | 14 -- src/StardewModdingAPI.Toolkit/ModToolkit.cs | 6 +- 12 files changed, 281 insertions(+), 253 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index e63057b3..9e91b993 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -142,7 +142,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ParsedModDataRecord + this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields { Status = ModStatus.AssumeBroken, AlternativeUrl = "http://example.org" @@ -526,7 +526,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, ParsedModDataRecord modRecord = null) + private void SetupMetadataForValidation(Mock mod, ModDataRecordVersionedFields 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 5a8689de..2145105b 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework IManifest Manifest { get; } /// Metadata about the mod from SMAPI's internal data (if any). - ParsedModDataRecord DataRecord { get; } + ModDataRecordVersionedFields 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 4db25932..585debb4 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// Metadata about the mod from SMAPI's internal data (if any). - public ParsedModDataRecord DataRecord { get; } + public ModDataRecordVersionedFields DataRecord { get; } /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } @@ -59,7 +59,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, ParsedModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index c1bc51ec..174820a1 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -51,7 +51,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // parse internal data record (if any) - ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); // get display name string displayName = manifest?.Name; @@ -301,7 +301,7 @@ namespace StardewModdingAPI.Framework.ModLoading string[] failedModNames = ( from entry in dependencies where entry.IsRequired && entry.Mod == null - let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + let displayName = modDatabase.Get(entry.ID)?.DisplayName ?? entry.ID let modUrl = modDatabase.GetModPageUrlFor(entry.ID) orderby displayName select modUrl != null diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs new file mode 100644 index 00000000..ef6d4dd9 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The SMAPI predefined metadata. + internal class MetadataModel + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs new file mode 100644 index 00000000..e2b3ec1d --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The raw mod metadata from SMAPI's internal mod list. + internal class ModDataModel + { + /********* + ** 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(); + + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + public IDictionary ExtensionData { get; set; } + + /// 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 the former mod IDs. + public IEnumerable GetFormerIDs() + { + if (this.FormerIDs != null) + { + foreach (string id in this.FormerIDs.Split('|')) + yield return id.Trim(); + } + } + + + /********* + ** 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/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 97ad0ca4..21c9cfca 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -1,107 +1,66 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Toolkit.Framework.ModData { - /// Raw mod metadata from SMAPI's internal mod list. + /// The parsed mod metadata from SMAPI's internal mod list. public 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 default display name. + public string DisplayName { get; } + /// The mod's current unique ID. - public string ID { get; set; } + public string ID { get; } /// 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; } + public string[] FormerIDs { get; } /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + public IDictionary MapLocalVersions { get; } /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + public IDictionary MapRemoteVersions { get; } /// 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 ModDataField[] Fields { get; } /********* ** Public methods *********/ - /// Get a parsed representation of the . - public IEnumerable GetFields() + /// Construct an instance. + /// The mod's default display name. + /// The raw data model. + internal ModDataRecord(string displayName, ModDataModel model) { - 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); - } + this.DisplayName = displayName; + this.ID = model.ID; + this.FormerIDs = model.GetFormerIDs().ToArray(); + this.MapLocalVersions = new Dictionary(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); + this.MapRemoteVersions = new Dictionary(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); + this.Fields = model.GetFields().ToArray(); + } - yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + /// Get whether the mod has (or previously had) the given ID. + /// The mod ID. + public bool HasID(string id) + { + // try main ID + if (this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + foreach (string formerID in this.FormerIDs) + { + if (formerID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; } + + return false; } /// Get a semantic local version for update checks. @@ -127,20 +86,39 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData : version; } - - /********* - ** Private methods - *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) { - if (this.ExtensionData != null) + ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; + foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; + 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; } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs new file mode 100644 index 00000000..3601fc53 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The versioned fields from a for a specific manifest. + public class ModDataRecordVersionedFields + { + /********* + ** 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 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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + { + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs index c60d2bcb..a12e3c67 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Properties *********/ /// The underlying mod data records indexed by default display name. - private readonly IDictionary Records; + private readonly ModDataRecord[] Records; /// Get an update URL for an update key (if valid). private readonly Func GetUpdateUrl; @@ -22,63 +22,23 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData *********/ /// Construct an empty instance. public ModDatabase() - : this(new Dictionary(), key => null) { } + : this(new ModDataRecord[0], key => null) { } /// Construct an instance. /// The underlying mod data records indexed by default display name. /// Get an update URL for an update key (if valid). - public ModDatabase(IDictionary records, Func getUpdateUrl) + public ModDatabase(IEnumerable records, Func getUpdateUrl) { - this.Records = records; + this.Records = records.ToArray(); this.GetUpdateUrl = getUpdateUrl; } - /// Get a parsed representation of the which match a given manifest. - /// The manifest to match. - public ParsedModDataRecord GetParsed(IManifest manifest) + /// Get a mod data record. + /// The unique mod ID. + public ModDataRecord Get(string modID) { - // get raw record - if (!this.TryGetRaw(manifest?.UniqueID, 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) - { - return this.TryGetRaw(id, out string displayName, out ModDataRecord _) - ? displayName + return !string.IsNullOrWhiteSpace(modID) + ? this.Records.FirstOrDefault(p => p.HasID(modID)) : null; } @@ -86,55 +46,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// 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); + ModDataRecord record = this.Get(id); + ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); if (updateKeyField == null) return null; // get update URL return this.GetUpdateUrl(updateKeyField.Value); } - - - /********* - ** Private models - *********/ - /// 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) - { - if (!string.IsNullOrWhiteSpace(id)) - { - foreach (var entry in this.Records) - { - displayName = entry.Key; - record = entry.Value; - - // try main ID - if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - if (record.FormerIDs != null) - { - foreach (string part in record.FormerIDs.Split('|')) - { - if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - } - } - } - - displayName = null; - record = null; - return false; - } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs deleted file mode 100644 index 74f11ea5..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// A parsed representation of the fields from a for a specific manifest. - public 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 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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) - { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : null; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs deleted file mode 100644 index 9553cca9..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The SMAPI predefined metadata. - internal class SMetadata - { - /******** - ** Accessors - ********/ - /// Extra metadata about mods. - public IDictionary ModData { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 1723991e..7b678f3d 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -48,8 +49,9 @@ namespace StardewModdingAPI.Toolkit /// Get an update URL for an update key (if valid). public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) { - SMetadata metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); - return new ModDatabase(metadata.ModData, getUpdateUrl); + MetadataModel metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); + ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); + return new ModDatabase(records, getUpdateUrl); } /// Get an update URL for an update key (if valid). -- cgit From 9f7b4e029668ccb7b05fb8b5b02b7a3998b05a80 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 00:55:45 -0400 Subject: add method to get all data records (#532) --- src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs index a12e3c67..3b98bcf1 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -33,6 +33,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData this.GetUpdateUrl = getUpdateUrl; } + /// Get all mod data records. + public IEnumerable GetAll() + { + return this.Records; + } + /// Get a mod data record. /// The unique mod ID. public ModDataRecord Get(string modID) -- cgit From db7247f36d11c870ec7b54bf6ea8b5b036065288 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 20:51:49 -0400 Subject: simplify mod DB method (#532) --- src/SMAPI/Program.cs | 2 +- src/StardewModdingAPI.Toolkit/ModToolkit.cs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6f1fe761..150ed34a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -415,7 +415,7 @@ namespace StardewModdingAPI // load mod data ModToolkit toolkit = new ModToolkit(); - ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath, toolkit.GetUpdateUrl); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); // load mods { diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 7b678f3d..18fe1ff3 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -46,17 +46,16 @@ namespace StardewModdingAPI.Toolkit /// Get SMAPI's internal mod database. /// The file path for the SMAPI metadata file. - /// Get an update URL for an update key (if valid). - public ModDatabase GetModDatabase(string metadataPath, Func getUpdateUrl) + public ModDatabase GetModDatabase(string metadataPath) { MetadataModel metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); - return new ModDatabase(records, getUpdateUrl); + return new ModDatabase(records, this.GetUpdateUrl); } /// Get an update URL for an update key (if valid). /// The update key. - internal string GetUpdateUrl(string updateKey) + public string GetUpdateUrl(string updateKey) { string[] parts = updateKey.Split(new[] { ':' }, 2); if (parts.Length != 2) -- cgit From 89ad599561a058289d8ea44e0c345f1a30a3a6ac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 21:41:00 -0400 Subject: add support for writing versions to JSON --- docs/release-notes.md | 1 + src/SMAPI.Web/Startup.cs | 4 ++ .../Converters/SemanticVersionConverter.cs | 60 ++++++++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index 062f902e..a8f6d851 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -44,6 +44,7 @@ * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. * Added absolute pixels to `ICursorPosition`. + * Added support for reading/writing `ISemanticVersion` to JSON. * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 58584348..ced6e1c7 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -56,6 +57,9 @@ namespace StardewModdingAPI.Web .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) .AddJsonOptions(options => { + foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) + options.SerializerSettings.Converters.Add(converter); + options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index 4f0949fa..9b2f5e7d 100644 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -1,17 +1,67 @@ +using System; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Toolkit.Serialisation.Converters { /// Handles deserialisation of . - internal class SemanticVersionConverter : SimpleReadOnlyConverter + internal class SemanticVersionConverter : JsonConverter { /********* - ** Protected methods + ** Accessors + *********/ + /// Get whether this converter can read JSON. + public override bool CanRead => true; + + /// Get whether this converter can write JSON. + public override bool CanWrite => true; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return typeof(ISemanticVersion).IsAssignableFrom(objectType); + } + + /// Reads 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) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader)); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + /// 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) + { + writer.WriteValue(value?.ToString()); + } + + + /********* + ** Private methods *********/ /// Read a JSON object. /// The JSON object to read. - /// The path to the current JSON node. - protected override ISemanticVersion ReadObject(JObject obj, string path) + private ISemanticVersion ReadObject(JObject obj) { int major = obj.ValueIgnoreCase("MajorVersion"); int minor = obj.ValueIgnoreCase("MinorVersion"); @@ -26,7 +76,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters /// Read a JSON string. /// The JSON string value. /// The path to the current JSON node. - protected override ISemanticVersion ReadString(string str, string path) + private ISemanticVersion ReadString(string str, string path) { if (string.IsNullOrWhiteSpace(str)) return null; -- cgit From 3f5a5e54041a641e30fc5cc899046953d9763da4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 22:01:04 -0400 Subject: use more structured API response for update checks (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 44 +++++++++++---------- .../Framework/ModRepositories/IModRepository.cs | 1 - .../Framework/ModRepositories/ModInfoModel.cs | 2 +- src/SMAPI/Program.cs | 16 +++----- .../Framework/Clients/WebApi/ModEntryModel.cs | 45 +++++++++++++++++++--- .../Clients/WebApi/ModEntryVersionModel.cs | 31 +++++++++++++++ .../ModData/ModDataRecordVersionedFields.cs | 9 +++-- 7 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 960602f4..c4f1023b 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -80,7 +80,11 @@ namespace StardewModdingAPI.Web.Controllers [HttpPost] public async Task> PostAsync([FromBody] ModSearchModel model) { - ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + // parse request data + ISemanticVersion apiVersion = this.GetApiVersion(); + ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray(); + + // perform checks IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) { @@ -119,11 +123,10 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + if (result.Main == null || result.Main.Version.IsOlderThan(version)) { result.Name = data.Name; - result.Url = data.Url; - result.Version = version.ToString(); + result.Main = new ModEntryVersionModel(version, data.Url); } } @@ -136,35 +139,34 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + if (result.Optional == null || result.Optional.Version.IsOlderThan(version)) { result.Name = result.Name ?? data.Name; - result.PreviewUrl = data.Url; - result.PreviewVersion = version.ToString(); + result.Optional = new ModEntryVersionModel(version, data.Url); } } } // fallback to preview if latest is invalid - if (result.Version == null && result.PreviewVersion != null) + if (result.Main == null && result.Optional != null) { - result.Version = result.PreviewVersion; - result.Url = result.PreviewUrl; - result.PreviewVersion = null; - result.PreviewUrl = null; + result.Main = result.Optional; + result.Optional = null; } // special cases if (mod.ID == "Pathoschild.SMAPI") { result.Name = "SMAPI"; - result.Url = "https://smapi.io/"; - if (result.PreviewUrl != null) - result.PreviewUrl = "https://smapi.io/"; + if (result.Main != null) + result.Main.Url = "https://smapi.io/"; + if (result.Optional != null) + result.Optional.Url = "https://smapi.io/"; } // add result result.Errors = errors.ToArray(); + result.SetBackwardsCompatibility(apiVersion); mods[mod.ID] = result; } @@ -199,7 +201,8 @@ namespace StardewModdingAPI.Web.Controllers /// Get the mods for which the API should return data. /// The search model. - private IEnumerable GetSearchMods(ModSearchModel model) + /// The requested API version. + private IEnumerable GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion) { if (model == null) yield break; @@ -212,7 +215,7 @@ namespace StardewModdingAPI.Web.Controllers } // yield mod update keys if backwards compatible - if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17")) { foreach (string updateKey in model.ModKeys.Distinct()) yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); @@ -247,12 +250,11 @@ namespace StardewModdingAPI.Web.Controllers }); } - /// Get whether the API should return data in a backwards compatible way. - /// The last version for which data should be backwards compatible. - private bool ShouldBeBackwardsCompatible(string maxVersion) + /// Get the requested API version. + private ISemanticVersion GetApiVersion() { string actualVersion = (string)this.RouteData.Values["version"]; - return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + return new SemanticVersion(actualVersion); } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 4c879c8d..09c59a86 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs index ccb0699c..18252298 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { /// Generic metadata about a mod. - public class ModInfoModel + internal class ModInfoModel { /********* ** Accessors diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 150ed34a..a1180474 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -596,8 +596,8 @@ namespace StardewModdingAPI try { ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; - ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; - ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; + ISemanticVersion latestStable = response.Main?.Version; + ISemanticVersion latestBeta = response.Optional?.Version; if (latestStable == null && response.Errors.Any()) { @@ -673,18 +673,14 @@ namespace StardewModdingAPI // parse versions ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - ISemanticVersion latestVersion = result.Version != null - ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) - : null; - ISemanticVersion optionalVersion = result.PreviewVersion != null - ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) - : null; + ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; + ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; // show update alerts if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) - updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) - updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); + updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); } // show update errors diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index e4ab168e..581a524c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -12,19 +14,50 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod name. public string Name { get; set; } + /// The main version. + public ModEntryVersionModel Main { get; set; } + + /// The latest optional version, if newer than . + public ModEntryVersionModel Optional { get; set; } + + /// The errors that occurred while fetching update data. + public string[] Errors { get; set; } = new string[0]; + + /**** + ** Backwards-compatible fields + ****/ /// The mod's latest version number. - public string Version { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Main))] + internal string Version { get; private set; } /// The mod's web URL. - public string Url { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Main))] + internal string Url { get; private set; } /// The mod's latest optional release, if newer than . - public string PreviewVersion { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Optional))] + internal string PreviewVersion { get; private set; } /// The web URL to the mod's latest optional release, if newer than . - public string PreviewUrl { get; set; } + [Obsolete("Use " + nameof(ModEntryModel.Optional))] + internal string PreviewUrl { get; private set; } - /// The errors that occurred while fetching update data. - public string[] Errors { get; set; } = new string[0]; + + /********* + ** Public methods + *********/ + /// Set backwards-compatible fields. + /// The requested API version. + public void SetBackwardsCompatibility(ISemanticVersion version) + { + if (version.IsOlderThan("2.6-beta.19")) + { + this.Version = this.Main?.Version?.ToString(); + this.Url = this.Main?.Url; + + this.PreviewVersion = this.Optional?.Version?.ToString(); + this.PreviewUrl = this.Optional?.Url; + } + } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs new file mode 100644 index 00000000..dadb8c10 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs @@ -0,0 +1,31 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a version. + public class ModEntryVersionModel + { + /********* + ** Accessors + *********/ + /// The version number. + public ISemanticVersion Version { get; set; } + + /// The mod page URL. + public string Url { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModEntryVersionModel() { } + + /// Construct an instance. + /// The version number. + /// The mod page URL. + public ModEntryVersionModel(ISemanticVersion version, string url) + { + this.Version = version; + this.Url = url; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 3601fc53..237f2c66 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -40,12 +40,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// Get a semantic remote version for update checks. /// The remote version to normalise. - public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version) { - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + if (version == null) + return null; + + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString()); return rawVersion != null ? new SemanticVersion(rawVersion) - : null; + : version; } } } -- cgit From c9fedebaf3231a2d5a00a95ff1ffd3ac5dac4ae2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 22:30:34 -0400 Subject: add support for unofficial version in update checks (#532) --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 47 +++++++++++++++++++++- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 3 ++ src/SMAPI.Web/appsettings.json | 4 +- src/SMAPI/Program.cs | 3 ++ .../Framework/Clients/WebApi/ModEntryModel.cs | 3 ++ 6 files changed, 58 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/docs/release-notes.md b/docs/release-notes.md index a8f6d851..14cf78a2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * Improved update checks: * added beta update channel; * added support for optional files on Nexus; + * added support for unofficial updates from the wiki (only if the installed version is incompatible); * added console warning for mods which don't have update checks configured; * fixed mod update checks failing if a mod only has prerelease versions on GitHub; * fixed Nexus mod update alerts not showing HTTPS links. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c4f1023b..b9af17dc 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -45,6 +46,9 @@ namespace StardewModdingAPI.Web.Controllers /// The internal mod metadata list. private readonly ModDatabase ModDatabase; + /// The web URL for the wiki compatibility list. + private readonly string WikiCompatibilityPageUrl; + /********* ** Public methods @@ -60,6 +64,7 @@ namespace StardewModdingAPI.Web.Controllers { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; + this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl; this.Cache = cache; this.SuccessCacheMinutes = config.SuccessCacheMinutes; @@ -84,6 +89,9 @@ namespace StardewModdingAPI.Web.Controllers ISemanticVersion apiVersion = this.GetApiVersion(); ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray(); + // fetch wiki data + WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); + // perform checks IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) @@ -123,7 +131,7 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Main == null || result.Main.Version.IsOlderThan(version)) + if (this.IsNewer(version, result.Main?.Version)) { result.Name = data.Name; result.Main = new ModEntryVersionModel(version, data.Url); @@ -139,7 +147,7 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Optional == null || result.Optional.Version.IsOlderThan(version)) + if (this.IsNewer(version, result.Optional?.Version)) { result.Name = result.Name ?? data.Name; result.Optional = new ModEntryVersionModel(version, data.Url); @@ -147,6 +155,13 @@ namespace StardewModdingAPI.Web.Controllers } } + // get unofficial version + { + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); + } + // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) { @@ -199,6 +214,14 @@ namespace StardewModdingAPI.Web.Controllers return true; } + /// Get whether a version is newer than an version. + /// The current version. + /// The other version. + private bool IsNewer(ISemanticVersion current, ISemanticVersion other) + { + return current != null && (other == null || other.IsOlderThan(current)); + } + /// Get the mods for which the API should return data. /// The search model. /// The requested API version. @@ -222,6 +245,26 @@ namespace StardewModdingAPI.Web.Controllers } } + /// Get mod data from the wiki compatibility list. + private async Task GetWikiDataAsync() + { + ModToolkit toolkit = new ModToolkit(); + return await this.Cache.GetOrCreateAsync($"_wiki", async entry => + { + try + { + WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync(); + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes); + return entries; + } + catch + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes); + return new WikiCompatibilityEntry[0]; + } + }); + } + /// Get the mod info for an update key. /// The namespaced update key. private async Task GetInfoForUpdateKeyAsync(string updateKey) diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index fc3b7dc2..ce4f3cb5 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -24,5 +24,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The repository key for Nexus Mods. public string NexusKey { get; set; } + + /// The web URL for the wiki compatibility list. + public string WikiCompatibilityPageUrl { get; set; } } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 2f87dbe5..837ba536 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -46,6 +46,8 @@ "ChucklefishKey": "Chucklefish", "GitHubKey": "GitHub", - "NexusKey": "Nexus" + "NexusKey": "Nexus", + + "WikiCompatibilityPageUrl": "https://smapi.io/compat" } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a1180474..d899e512 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -675,12 +675,15 @@ namespace StardewModdingAPI ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; + ISemanticVersion unofficialVersion = result.Unofficial?.Version; // show update alerts if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); + else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) + updates.Add(Tuple.Create(mod, unofficialVersion, result.Unofficial?.Url)); } // show update errors diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 581a524c..adfdfef9 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest optional version, if newer than . public ModEntryVersionModel Optional { get; set; } + /// The latest unofficial version, if newer than and . + public ModEntryVersionModel Unofficial { get; set; } + /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; -- cgit From a0888e0ad1bf0ed38982d2aadf78c31d046b061b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:01:57 -0400 Subject: add optional extended metadata to mods API (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 19 ++---- .../Framework/Clients/WebApi/ModEntryModel.cs | 6 +- .../Clients/WebApi/ModExtendedMetadataModel.cs | 77 ++++++++++++++++++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 3 + .../Framework/ModData/ModDataRecord.cs | 10 +++ 5 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index b9af17dc..e5ae3fc7 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -132,10 +132,7 @@ namespace StardewModdingAPI.Web.Controllers } if (this.IsNewer(version, result.Main?.Version)) - { - result.Name = data.Name; result.Main = new ModEntryVersionModel(version, data.Url); - } } // handle optional version @@ -148,19 +145,14 @@ namespace StardewModdingAPI.Web.Controllers } if (this.IsNewer(version, result.Optional?.Version)) - { - result.Name = result.Name ?? data.Name; result.Optional = new ModEntryVersionModel(version, data.Url); - } } } // get unofficial version - { - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); - if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - } + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) @@ -172,13 +164,16 @@ namespace StardewModdingAPI.Web.Controllers // special cases if (mod.ID == "Pathoschild.SMAPI") { - result.Name = "SMAPI"; if (result.Main != null) result.Main.Url = "https://smapi.io/"; if (result.Optional != null) result.Optional.Url = "https://smapi.io/"; } + // add extended metadata + if (model.IncludeExtendedMetadata && (wikiEntry != null || record != null)) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // add result result.Errors = errors.ToArray(); result.SetBackwardsCompatibility(apiVersion); diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index adfdfef9..b311bd3b 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -11,9 +11,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod's unique ID (if known). public string ID { get; set; } - /// The mod name. - public string Name { get; set; } - /// The main version. public ModEntryVersionModel Main { get; set; } @@ -23,6 +20,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest unofficial version, if newer than and . public ModEntryVersionModel Unofficial { get; set; } + /// Optional extended data which isn't needed for update checks. + public ModExtendedMetadataModel Metadata { get; set; } + /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs new file mode 100644 index 00000000..a716114b --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -0,0 +1,77 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Extended metadata about a mod. + public class ModExtendedMetadataModel + { + /********* + ** Accessors + *********/ + /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + public string[] ID { get; set; } = new string[0]; + + /// The mod's display name. + public string Name { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + /// The compatibility status. + [JsonConverter(typeof(StringEnumConverter))] + public WikiCompatibilityStatus? CompatibilityStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + public string CompatibilitySummary { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModExtendedMetadataModel() { } + + /// Construct an instance. + /// The mod metadata from the wiki (if available). + /// The mod metadata from SMAPI's internal DB (if available). + public ModExtendedMetadataModel(WikiCompatibilityEntry wiki, ModDataRecord db) + { + // wiki data + if (wiki != null) + { + this.ID = wiki.ID; + this.Name = wiki.Name; + this.NexusID = wiki.NexusID; + this.ChucklefishID = wiki.ChucklefishID; + this.GitHubRepo = wiki.GitHubRepo; + this.CustomSourceUrl = wiki.CustomSourceUrl; + this.CustomUrl = wiki.CustomUrl; + this.CompatibilityStatus = wiki.Status; + this.CompatibilitySummary = wiki.Summary; + } + + // internal DB data + if (db != null) + { + this.ID = this.ID.Union(db.FormerIDs).ToArray(); + this.Name = this.Name ?? db.DisplayName; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index ffca32ca..754cf02c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mods for which to find data. public ModSearchEntryModel[] Mods { get; set; } + /// Whether to include extended metadata for each mod. + public bool IncludeExtendedMetadata { get; set; } + /********* ** Public methods diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 21c9cfca..82ac8837 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -86,6 +86,16 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData : version; } + /// Get the possible mod IDs. + public IEnumerable GetIDs() + { + return this.FormerIDs + .Concat(new[] { this.ID }) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .Distinct(); + } + /// Get a parsed representation of the which match a given manifest. /// The manifest to match. public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) -- cgit From 60b38666e29684650108031f08ca30bfe483ceab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:27:31 -0400 Subject: simplify mod API response structure (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 154 +++++++++++---------- .../Framework/Clients/WebApi/WebApiClient.cs | 5 +- 2 files changed, 87 insertions(+), 72 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index e5ae3fc7..b500e19d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -83,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model) + public async Task PostAsync([FromBody] ModSearchModel model) { // parse request data ISemanticVersion apiVersion = this.GetApiVersion(); @@ -92,101 +92,115 @@ namespace StardewModdingAPI.Web.Controllers // fetch wiki data WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); - // perform checks + // fetch data IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) { if (string.IsNullOrWhiteSpace(mod.ID)) continue; - // resolve update keys - var updateKeys = new HashSet(mod.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); - ModDataRecord record = this.ModDatabase.Get(mod.ID); - if (record?.Fields != null) + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); + result.SetBackwardsCompatibility(apiVersion); + mods[mod.ID] = result; + } + + // return in expected structure + return apiVersion.IsNewerThan("2.6-beta.18") + ? mods.Values + : (object)mods; + } + + + /********* + ** Private methods + *********/ + /// Get the metadata for a mod. + /// The mod data to match. + /// The wiki data. + /// Whether to include extended metadata for each mod. + /// Returns the mod data if found, else null. + private async Task GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata) + { + // resolve update keys + var updateKeys = new HashSet(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + ModDataRecord record = this.ModDatabase.Get(search.ID); + if (record?.Fields != null) + { + string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) + updateKeys.Add(defaultUpdateKey); + } + + // get latest versions + ModEntryModel result = new ModEntryModel { ID = search.ID }; + IList errors = new List(); + foreach (string updateKey in updateKeys) + { + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) { - string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; - if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) - updateKeys.Add(defaultUpdateKey); + errors.Add(data.Error); + continue; } - // get latest versions - ModEntryModel result = new ModEntryModel { ID = mod.ID }; - IList errors = new List(); - foreach (string updateKey in updateKeys) + // handle main version + if (data.Version != null) { - // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); - if (data.Error != null) + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) { - errors.Add(data.Error); + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } - // handle main version - if (data.Version != null) - { - if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); - continue; - } - - if (this.IsNewer(version, result.Main?.Version)) - result.Main = new ModEntryVersionModel(version, data.Url); - } + if (this.IsNewer(version, result.Main?.Version)) + result.Main = new ModEntryVersionModel(version, data.Url); + } - // handle optional version - if (data.PreviewVersion != null) + // handle optional version + if (data.PreviewVersion != null) + { + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) { - if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); - continue; - } - - if (this.IsNewer(version, result.Optional?.Version)) - result.Optional = new ModEntryVersionModel(version, data.Url); + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; } - } - // get unofficial version - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); - if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - - // fallback to preview if latest is invalid - if (result.Main == null && result.Optional != null) - { - result.Main = result.Optional; - result.Optional = null; + if (this.IsNewer(version, result.Optional?.Version)) + result.Optional = new ModEntryVersionModel(version, data.Url); } + } - // special cases - if (mod.ID == "Pathoschild.SMAPI") - { - if (result.Main != null) - result.Main.Url = "https://smapi.io/"; - if (result.Optional != null) - result.Optional.Url = "https://smapi.io/"; - } + // get unofficial version + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - // add extended metadata - if (model.IncludeExtendedMetadata && (wikiEntry != null || record != null)) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // fallback to preview if latest is invalid + if (result.Main == null && result.Optional != null) + { + result.Main = result.Optional; + result.Optional = null; + } - // add result - result.Errors = errors.ToArray(); - result.SetBackwardsCompatibility(apiVersion); - mods[mod.ID] = result; + // special cases + if (result.ID == "Pathoschild.SMAPI") + { + if (result.Main != null) + result.Main.Url = "https://smapi.io/"; + if (result.Optional != null) + result.Optional.Url = "https://smapi.io/"; } - return mods; - } + // add extended metadata + if (includeExtendedMetadata && (wikiEntry != null || record != null)) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // add result + result.Errors = errors.ToArray(); + return result; + } - /********* - ** Private methods - *********/ /// Parse a namespaced mod ID. /// The raw mod ID to parse. /// The parsed vendor key. diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 892dfeba..3e412fc3 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using Newtonsoft.Json; @@ -34,10 +35,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod keys for which to fetch the latest version. public IDictionary GetModInfo(params ModSearchEntryModel[] mods) { - return this.Post>( + return this.Post( $"v{this.Version}/mods", new ModSearchModel(mods) - ); + ).ToDictionary(p => p.ID); } -- cgit From c0370c54113bc95919affcbfbba8720a42b97a30 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:50:06 -0400 Subject: add includeExtendedMetadata option to toolkit client (#532) --- src/SMAPI/Program.cs | 2 +- .../Framework/Clients/WebApi/ModSeachModel.cs | 4 +++- .../Framework/Clients/WebApi/WebApiClient.cs | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index d899e512..a88db105 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -595,7 +595,7 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; ISemanticVersion latestStable = response.Main?.Version; ISemanticVersion latestBeta = response.Optional?.Version; diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index 754cf02c..df0d8457 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -31,9 +31,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The mods to search. - public ModSearchModel(ModSearchEntryModel[] mods) + /// Whether to include extended metadata for each mod. + public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) { this.Mods = mods.ToArray(); + this.IncludeExtendedMetadata = includeExtendedMetadata; } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 3e412fc3..5bbe473e 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -33,11 +33,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Get metadata about a set of mods from the web API. /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params ModSearchEntryModel[] mods) + /// Whether to include extended metadata for each mod. + public IDictionary GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) { return this.Post( $"v{this.Version}/mods", - new ModSearchModel(mods) + new ModSearchModel(mods, includeExtendedMetadata) ).ToDictionary(p => p.ID); } -- cgit From 86428a31c2c275b32f08f149157f2fad78c8e488 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:54:49 -0400 Subject: fix web API client not using correct JSON settings (#532) --- .../Framework/Clients/WebApi/WebApiClient.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 5bbe473e..0ecd9664 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -18,6 +19,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The API version number. private readonly ISemanticVersion Version; + /// The JSON serializer settings to use. + private readonly JsonSerializerSettings JsonSettings = new JsonHelper().JsonSettings; + /********* ** Public methods @@ -62,7 +66,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi client.Headers["Content-Type"] = "application/json"; client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response); + return JsonConvert.DeserializeObject(response, this.JsonSettings); } } } -- cgit From c12777ad53583997dd9e1ee074e8376da827fc84 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Jun 2018 21:00:45 -0400 Subject: move basic mod scanning into the toolkit (#532) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 8 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 40 ++------ src/SMAPI/Program.cs | 16 ++-- .../Framework/ModScanning/ModFolder.cs | 60 ++++++++++++ .../Framework/ModScanning/ModScanner.cs | 103 +++++++++++++++++++++ src/StardewModdingAPI.Toolkit/ModToolkit.cs | 16 ++++ 6 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs create mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 9e91b993..a38621f8 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -7,8 +7,8 @@ using Newtonsoft.Json; using NUnit.Framework; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; namespace StardewModdingAPI.Tests.Core @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(rootFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); @@ -46,7 +46,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(modFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -85,7 +85,7 @@ namespace StardewModdingAPI.Tests.Core File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 174820a1..9ac95fd4 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; @@ -17,38 +18,15 @@ namespace StardewModdingAPI.Framework.ModLoading ** Public methods *********/ /// Get manifest metadata for each folder in the given root path. + /// The mod toolkit. /// The root path to search for mods. - /// The JSON helper with which to read manifests. /// Handles access to SMAPI's internal mod metadata list. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) + public IEnumerable ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase) { - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) + foreach (ModFolder folder in toolkit.GetModFolders(rootPath)) { - // read file - Manifest manifest = null; - string error = null; - { - string path = Path.Combine(modDir.FullName, "manifest.json"); - try - { - manifest = jsonHelper.ReadJsonFile(path); - if (manifest == null) - { - error = File.Exists(path) - ? "its manifest is invalid." - : "it doesn't have a manifest."; - } - } - catch (SParseException ex) - { - error = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; - } - } + Manifest manifest = folder.Manifest; // parse internal data record (if any) ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); @@ -58,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (string.IsNullOrWhiteSpace(displayName)) displayName = dataRecord?.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) - displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); + displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName); // apply defaults if (manifest != null && dataRecord != null) @@ -68,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading } // build metadata - ModMetadataStatus status = error == null + ModMetadataStatus status = folder.ManifestParseError == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); + yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError); } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a88db105..c9266c69 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -104,8 +104,8 @@ namespace StardewModdingAPI new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper = new JsonHelper(); + /// The mod toolkit used for generic mod interactions. + private readonly ModToolkit Toolkit = new ModToolkit(); /********* @@ -205,7 +205,7 @@ namespace StardewModdingAPI new RectangleConverter() }; foreach (JsonConverter converter in converters) - this.JsonHelper.JsonSettings.Converters.Add(converter); + this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); // add error handlers #if SMAPI_FOR_WINDOWS @@ -423,14 +423,14 @@ namespace StardewModdingAPI ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(toolkit, Constants.ModPath, modDatabase).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); // process dependencies mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods - this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase); + this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); // write metadata file if (this.Settings.DumpMetadata) @@ -443,7 +443,7 @@ namespace StardewModdingAPI ModFolderPath = Constants.ModPath, Mods = mods }; - this.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export); + this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export); } // check for updates @@ -875,7 +875,7 @@ namespace StardewModdingAPI { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); @@ -1117,7 +1117,7 @@ namespace StardewModdingAPI /// The mods for which to reload translations. private void ReloadTranslations(IEnumerable mods) { - JsonHelper jsonHelper = this.JsonHelper; + JsonHelper jsonHelper = this.Toolkit.JsonHelper; foreach (IModMetadata metadata in mods) { if (metadata.IsContentPack) diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs new file mode 100644 index 00000000..9b6853b4 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// The info about a mod read from its folder. + public class ModFolder + { + /********* + ** Accessors + *********/ + /// The Mods subfolder containing this mod. + public DirectoryInfo SearchDirectory { get; } + + /// The folder containing manifest.json. + public DirectoryInfo ActualDirectory { get; } + + /// The mod manifest. + public Manifest Manifest { get; } + + /// The error which occurred parsing the manifest, if any. + public string ManifestParseError { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance when a mod wasn't found in a folder. + /// The directory that was searched. + public ModFolder(DirectoryInfo searchDirectory) + { + this.SearchDirectory = searchDirectory; + } + + /// Construct an instance. + /// The Mods subfolder containing this mod. + /// The folder containing manifest.json. + /// The mod manifest. + /// The error which occurred parsing the manifest, if any. + public ModFolder(DirectoryInfo searchDirectory, DirectoryInfo actualDirectory, Manifest manifest, string manifestParseError = null) + { + this.SearchDirectory = searchDirectory; + this.ActualDirectory = actualDirectory; + this.Manifest = manifest; + this.ManifestParseError = manifestParseError; + } + + /// Get the update keys for a mod. + /// The mod manifest. + public IEnumerable GetUpdateKeys(Manifest manifest) + { + return + (manifest.UpdateKeys ?? new string[0]) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .ToArray(); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs new file mode 100644 index 00000000..d3662c9c --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// Scans folders for mod data. + public class ModScanner + { + /********* + ** Properties + *********/ + /// The JSON helper with which to read manifests. + private readonly JsonHelper JsonHelper; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The JSON helper with which to read manifests. + public ModScanner(JsonHelper jsonHelper) + { + this.JsonHelper = jsonHelper; + } + + /// Extract information about all mods in the given folder. + /// The root folder containing mods. + public IEnumerable GetModFolders(string rootPath) + { + foreach (DirectoryInfo folder in new DirectoryInfo(rootPath).EnumerateDirectories()) + yield return this.ReadFolder(rootPath, folder); + } + + /// Extract information from a mod folder. + /// The root folder containing mods. + /// The folder to search for a mod. + public ModFolder ReadFolder(string rootPath, DirectoryInfo searchFolder) + { + // find manifest.json + FileInfo manifestFile = this.FindManifest(searchFolder); + if (manifestFile == null) + return new ModFolder(searchFolder); + + // read mod info + Manifest manifest = null; + string manifestError = null; + { + try + { + manifest = this.JsonHelper.ReadJsonFile(manifestFile.FullName); + if (manifest == null) + { + manifestError = File.Exists(manifestFile.FullName) + ? "its manifest is invalid." + : "it doesn't have a manifest."; + } + } + catch (SParseException ex) + { + manifestError = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + manifestError = $"parsing its manifest failed:\n{ex}"; + } + } + + return new ModFolder(searchFolder, manifestFile.Directory, manifest, manifestError); + } + + + /********* + ** Private methods + *********/ + /// Find the manifest for a mod folder. + /// The folder to search. + private FileInfo FindManifest(DirectoryInfo folder) + { + while (true) + { + // check for manifest in current folder + FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json")); + if (file.Exists) + return file; + + // check for single subfolder + FileSystemInfo[] entries = folder.EnumerateFileSystemInfos().Take(2).ToArray(); + if (entries.Length == 1 && entries[0] is DirectoryInfo subfolder) + { + folder = subfolder; + continue; + } + + // not found + return null; + } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs index 18fe1ff3..8c78b2f3 100644 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Serialisation; namespace StardewModdingAPI.Toolkit { @@ -27,6 +29,13 @@ namespace StardewModdingAPI.Toolkit }; + /********* + ** Accessors + *********/ + /// Encapsulates SMAPI's JSON parsing. + public JsonHelper JsonHelper { get; } = new JsonHelper(); + + /********* ** Public methods *********/ @@ -53,6 +62,13 @@ namespace StardewModdingAPI.Toolkit return new ModDatabase(records, this.GetUpdateUrl); } + /// Extract information about all mods in the given folder. + /// The root folder containing mods. + public IEnumerable GetModFolders(string rootPath) + { + return new ModScanner(this.JsonHelper).GetModFolders(rootPath); + } + /// Get an update URL for an update key (if valid). /// The update key. public string GetUpdateUrl(string updateKey) -- cgit From 34c43f9f66b33c402947be5e84544e09cb048290 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Jul 2018 12:23:03 -0400 Subject: add toolkit method for API data (#532) --- .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index a716114b..21376b36 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -73,5 +74,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Name = this.Name ?? db.DisplayName; } } + + /// Get update keys based on the metadata. + public IEnumerable GetUpdateKeys() + { + if (this.NexusID.HasValue) + yield return $"Nexus:{this.NexusID}"; + if (this.ChucklefishID.HasValue) + yield return $"Chucklefish:{this.ChucklefishID}"; + if (this.GitHubRepo != null) + yield return $"GitHub:{this.GitHubRepo}"; + } } } -- cgit From 703acdc63f8543bab01f38ea68c28befb2911df8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Jul 2018 21:33:53 -0400 Subject: fix backwards-compatible API fields not being serialised (#532) --- .../Framework/Clients/WebApi/ModEntryModel.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index b311bd3b..f3f22b93 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -31,18 +32,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ****/ /// The mod's latest version number. [Obsolete("Use " + nameof(ModEntryModel.Main))] + [JsonProperty] internal string Version { get; private set; } /// The mod's web URL. [Obsolete("Use " + nameof(ModEntryModel.Main))] + [JsonProperty] internal string Url { get; private set; } /// The mod's latest optional release, if newer than . [Obsolete("Use " + nameof(ModEntryModel.Optional))] + [JsonProperty] internal string PreviewVersion { get; private set; } /// The web URL to the mod's latest optional release, if newer than . [Obsolete("Use " + nameof(ModEntryModel.Optional))] + [JsonProperty] internal string PreviewUrl { get; private set; } -- cgit From 34b1dcc1f75651c967257d37a0f29f24335c8110 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 3 Jul 2018 01:59:45 -0400 Subject: fix missing manifest not marking mod invalid (#532) --- src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 7 ------- src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 8 ++------ 2 files changed, 2 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 9b6853b4..4aaa3f83 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -27,13 +27,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /********* ** Public methods *********/ - /// Construct an instance when a mod wasn't found in a folder. - /// The directory that was searched. - public ModFolder(DirectoryInfo searchDirectory) - { - this.SearchDirectory = searchDirectory; - } - /// Construct an instance. /// The Mods subfolder containing this mod. /// The folder containing manifest.json. diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs index d3662c9c..de8d0f02 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // find manifest.json FileInfo manifestFile = this.FindManifest(searchFolder); if (manifestFile == null) - return new ModFolder(searchFolder); + return new ModFolder(searchFolder, null, null, "it doesn't have a manifest."); // read mod info Manifest manifest = null; @@ -53,11 +53,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { manifest = this.JsonHelper.ReadJsonFile(manifestFile.FullName); if (manifest == null) - { - manifestError = File.Exists(manifestFile.FullName) - ? "its manifest is invalid." - : "it doesn't have a manifest."; - } + manifestError = "its manifest is invalid."; } catch (SParseException ex) { -- cgit From 4f854aea1530177f959fc01b1731ae4759830321 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Jul 2018 22:50:35 -0400 Subject: fix various build issues - installer not waiting until SaveBackup mod is compiled before preparing release build; - missing XML doc files for new toolkit assemblies; - missing XML doc file in SMAPI release build; - SaveBackup including toolkit DLL. --- build/prepare-install-package.targets | 4 ++++ src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj | 1 + src/SMAPI.sln | 1 + src/SMAPI/StardewModdingAPI.csproj | 2 +- .../StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 6 ++---- src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 4 +--- 6 files changed, 10 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit') diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 33a92f71..79185896 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,8 +41,10 @@ + + @@ -58,8 +60,10 @@ + + diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index afba15a1..0ccbcc6c 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -54,6 +54,7 @@ {d5cfd923-37f1-4bc3-9be8-e506e202ac28} StardewModdingAPI.Toolkit.CoreInterfaces + False diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 99e93d62..d870c30c 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" ProjectSection(ProjectDependencies) = postProject + {E272EB5D-8C57-417E-8E60-C1079D3F53C4} = {E272EB5D-8C57-417E-8E60-C1079D3F53C4} {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 27e98f3a..0d0a5fe9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -43,7 +43,7 @@ x86 false $(SolutionDir)\..\bin\Release\SMAPI - $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml + $(SolutionDir)\..\bin\Release\SMAPI\StardewModdingAPI.xml TRACE true true diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj index e003122e..525931e5 100644 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj +++ b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -2,12 +2,10 @@ net4.5;netstandard2.0 + StardewModdingAPI false - - - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces - StardewModdingAPI + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 904f6786..21c130b3 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -3,10 +3,8 @@ net4.5;netstandard2.0 false - - - ..\..\bin\$(Configuration)\SMAPI.Toolkit + ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml -- cgit