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) --- .../Clients/Wiki/WikiCompatibilityClient.cs | 142 +++++++++++++++++++++ .../Clients/Wiki/WikiCompatibilityEntry.cs | 24 ++++ .../Clients/Wiki/WikiCompatibilityStatus.cs | 24 ++++ 3 files changed, 190 insertions(+) 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 (limited to 'src/StardewModdingAPI.Toolkit/Framework') 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 + } +} -- 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/Framework') 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/Framework') 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) --- .../Framework/Clients/WebApi/ModInfoModel.cs | 56 +++++++++++++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 37 ++++++++++++ .../Framework/Clients/WebApi/WebApiClient.cs | 70 ++++++++++++++++++++++ 3 files changed, 163 insertions(+) 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/Framework') 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 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) --- .../Framework/Clients/WebApi/WebApiClient.cs | 34 +++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit/Framework') 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 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) --- .../Framework/Clients/WebApi/ModInfoModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI.Toolkit/Framework') 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 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) --- .../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 ++------------- 5 files changed, 76 insertions(+), 98 deletions(-) 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/Framework') 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/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI.Toolkit/Framework') 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 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) --- .../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 ++ 7 files changed, 469 insertions(+) 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/Framework') 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 Ge