summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI.Toolkit/Framework/Clients
diff options
context:
space:
mode:
Diffstat (limited to 'src/StardewModdingAPI.Toolkit/Framework/Clients')
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs6
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs29
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs230
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs161
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs36
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs24
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs48
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs18
8 files changed, 351 insertions, 201 deletions
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
index 2aafe199..8a9c0a25 100644
--- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
@@ -18,9 +18,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
public ModEntryVersionModel Unofficial { get; set; }
+ /// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
+ public ModEntryVersionModel UnofficialForBeta { get; set; }
+
/// <summary>Optional extended data which isn't needed for update checks.</summary>
public ModExtendedMetadataModel Metadata { get; set; }
+ /// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary>
+ public bool HasBetaInfo { get; set; }
+
/// <summary>The errors that occurred while fetching update data.</summary>
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
index 21376b36..247730d7 100644
--- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
@@ -13,6 +13,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/*********
** Accessors
*********/
+ /****
+ ** Mod info
+ ****/
/// <summary>The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates).</summary>
public string[] ID { get; set; } = new string[0];
@@ -34,6 +37,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The custom mod page URL (if applicable).</summary>
public string CustomUrl { get; set; }
+ /****
+ ** Stable compatibility
+ ****/
/// <summary>The compatibility status.</summary>
[JsonConverter(typeof(StringEnumConverter))]
public WikiCompatibilityStatus? CompatibilityStatus { get; set; }
@@ -42,6 +48,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public string CompatibilitySummary { get; set; }
+ /****
+ ** Beta compatibility
+ ****/
+ /// <summary>The compatibility status for the Stardew Valley beta (if any).</summary>
+ [JsonConverter(typeof(StringEnumConverter))]
+ public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; }
+
+ /// <summary>The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatitng.</summary>
+ public string BetaCompatibilitySummary { get; set; }
+
+
/*********
** Public methods
*********/
@@ -51,20 +68,24 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an instance.</summary>
/// <param name="wiki">The mod metadata from the wiki (if available).</param>
/// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param>
- public ModExtendedMetadataModel(WikiCompatibilityEntry wiki, ModDataRecord db)
+ public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db)
{
// wiki data
if (wiki != null)
{
this.ID = wiki.ID;
- this.Name = wiki.Name;
+ this.Name = wiki.Name.FirstOrDefault();
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;
+
+ this.CompatibilityStatus = wiki.Compatibility.Status;
+ this.CompatibilitySummary = wiki.Compatibility.Summary;
+
+ this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status;
+ this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary;
}
// internal DB data
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
new file mode 100644
index 00000000..7197bf2c
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using HtmlAgilityPack;
+using Pathoschild.Http.Client;
+
+namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
+{
+ /// <summary>An HTTP client for fetching mod metadata from the wiki.</summary>
+ public class WikiClient : IDisposable
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying HTTP client.</summary>
+ private readonly IClient Client;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="userAgent">The user agent for the wiki API.</param>
+ /// <param name="baseUrl">The base URL for the wiki API.</param>
+ public WikiClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php")
+ {
+ this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
+ }
+
+ /// <summary>Fetch mods from the compatibility list.</summary>
+ public async Task<WikiModList> FetchModsAsync()
+ {
+ // fetch HTML
+ ResponseModel response = await this.Client
+ .GetAsync("")
+ .WithArguments(new
+ {
+ action = "parse",
+ page = "Modding:SMAPI_compatibility",
+ format = "json"
+ })
+ .As<ResponseModel>();
+ string html = response.Parse.Text["*"];
+
+ // parse HTML
+ var doc = new HtmlDocument();
+ doc.LoadHtml(html);
+
+ // fetch game versions
+ string stableVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-stable-version']")?.InnerText;
+ string betaVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-beta-version']")?.InnerText;
+
+ // 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
+ WikiModEntry[] mods = this.ParseEntries(modNodes).ToArray();
+ return new WikiModList
+ {
+ StableVersion = stableVersion,
+ BetaVersion = betaVersion,
+ Mods = mods
+ };
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ this.Client?.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Parse valid mod compatibility entries.</summary>
+ /// <param name="nodes">The HTML compatibility entries.</param>
+ private IEnumerable<WikiModEntry> ParseEntries(IEnumerable<HtmlNode> nodes)
+ {
+ foreach (HtmlNode node in nodes)
+ {
+ // extract fields
+ string[] names = this.GetAttributeAsCsv(node, "data-name");
+ string[] authors = this.GetAttributeAsCsv(node, "data-author");
+ string[] ids = this.GetAttributeAsCsv(node, "data-id");
+ string[] warnings = this.GetAttributeAsCsv(node, "data-warnings");
+ int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id");
+ int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id");
+ string githubRepo = this.GetAttribute(node, "data-github");
+ string customSourceUrl = this.GetAttribute(node, "data-custom-source");
+ string customUrl = this.GetAttribute(node, "data-url");
+ string anchor = this.GetAttribute(node, "id");
+
+ // parse stable compatibility
+ WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
+ {
+ Status = this.GetAttributeAsStatus(node, "data-status") ?? WikiCompatibilityStatus.Ok,
+ BrokeIn = this.GetAttribute(node, "data-broke-in"),
+ UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"),
+ UnofficialUrl = this.GetAttribute(node, "data-unofficial-url"),
+ Summary = this.GetInnerHtml(node, "mod-summary")?.Trim()
+ };
+
+ // parse beta compatibility
+ WikiCompatibilityInfo betaCompatibility = null;
+ {
+ WikiCompatibilityStatus? betaStatus = this.GetAttributeAsStatus(node, "data-beta-status");
+ if (betaStatus.HasValue)
+ {
+ betaCompatibility = new WikiCompatibilityInfo
+ {
+ Status = betaStatus.Value,
+ BrokeIn = this.GetAttribute(node, "data-beta-broke-in"),
+ UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"),
+ UnofficialUrl = this.GetAttribute(node, "data-beta-unofficial-url"),
+ Summary = this.GetInnerHtml(node, "mod-beta-summary")
+ };
+ }
+ }
+
+ // yield model
+ yield return new WikiModEntry
+ {
+ ID = ids,
+ Name = names,
+ Author = authors,
+ NexusID = nexusID,
+ ChucklefishID = chucklefishID,
+ GitHubRepo = githubRepo,
+ CustomSourceUrl = customSourceUrl,
+ CustomUrl = customUrl,
+ Compatibility = compatibility,
+ BetaCompatibility = betaCompatibility,
+ Warnings = warnings,
+ Anchor = anchor
+ };
+ }
+ }
+
+ /// <summary>Get an attribute value.</summary>
+ /// <param name="element">The element whose attributes to read.</param>
+ /// <param name="name">The attribute name.</param>
+ private string GetAttribute(HtmlNode element, string name)
+ {
+ string value = element.GetAttributeValue(name, null);
+ if (string.IsNullOrWhiteSpace(value))
+ return null;
+
+ return WebUtility.HtmlDecode(value);
+ }
+
+ /// <summary>Get an attribute value and parse it as a comma-delimited list of strings.</summary>
+ /// <param name="element">The element whose attributes to read.</param>
+ /// <param name="name">The attribute name.</param>
+ private string[] GetAttributeAsCsv(HtmlNode element, string name)
+ {
+ string raw = this.GetAttribute(element, name);
+ return !string.IsNullOrWhiteSpace(raw)
+ ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray()
+ : new string[0];
+ }
+
+ /// <summary>Get an attribute value and parse it as a compatibility status.</summary>
+ /// <param name="element">The element whose attributes to read.</param>
+ /// <param name="name">The attribute name.</param>
+ private WikiCompatibilityStatus? GetAttributeAsStatus(HtmlNode element, string name)
+ {
+ string raw = this.GetAttribute(element, name);
+ if (raw == null)
+ return null;
+ if (!Enum.TryParse(raw, true, out WikiCompatibilityStatus status))
+ throw new InvalidOperationException($"Unknown status '{raw}' when parsing compatibility list.");
+ return status;
+ }
+
+ /// <summary>Get an attribute value and parse it as a semantic version.</summary>
+ /// <param name="element">The element whose attributes to read.</param>
+ /// <param name="name">The attribute name.</param>
+ private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name)
+ {
+ string raw = this.GetAttribute(element, name);
+ return SemanticVersion.TryParse(raw, out ISemanticVersion version)
+ ? version
+ : null;
+ }
+
+ /// <summary>Get an attribute value and parse it as a nullable int.</summary>
+ /// <param name="element">The element whose attributes to read.</param>
+ /// <param name="name">The attribute name.</param>
+ private int? GetAttributeAsNullableInt(HtmlNode element, string name)
+ {
+ string raw = this.GetAttribute(element, name);
+ if (raw != null && int.TryParse(raw, out int value))
+ return value;
+ return null;
+ }
+
+ /// <summary>Get the text of an element with the given class name.</summary>
+ /// <param name="container">The metadata container.</param>
+ /// <param name="className">The field name.</param>
+ private string GetInnerHtml(HtmlNode container, string className)
+ {
+ return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml;
+ }
+
+ /// <summary>The response model for the MediaWiki parse API.</summary>
+ [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ private class ResponseModel
+ {
+ /// <summary>The parse API results.</summary>
+ public ResponseParseModel Parse { get; set; }
+ }
+
+ /// <summary>The inner response model for the MediaWiki parse API.</summary>
+ [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
+ [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ private class ResponseParseModel
+ {
+ /// <summary>The parsed text.</summary>
+ public IDictionary<string, string> Text { get; set; }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs
deleted file mode 100644
index d0da42df..00000000
--- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-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
-{
- /// <summary>An HTTP client for fetching mod metadata from the wiki compatibility list.</summary>
- public class WikiCompatibilityClient : IDisposable
- {
- /*********
- ** Properties
- *********/
- /// <summary>The underlying HTTP client.</summary>
- private readonly IClient Client;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="userAgent">The user agent for the wiki API.</param>
- /// <param name="baseUrl">The base URL for the wiki API.</param>
- public WikiCompatibilityClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php")
- {
- this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
- }
-
- /// <summary>Fetch mod compatibility entries.</summary>
- public async Task<WikiCompatibilityEntry[]> FetchAsync()
- {
- // fetch HTML
- ResponseModel response = await this.Client
- .GetAsync("")
- .WithArguments(new
- {
- action = "parse",
- page = "Modding:SMAPI_compatibility",
- format = "json"
- })
- .As<ResponseModel>();
- 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();
- }
-
- /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
- public void Dispose()
- {
- this.Client?.Dispose();
- }
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>Parse valid mod compatibility entries.</summary>
- /// <param name="nodes">The HTML compatibility entries.</param>
- private IEnumerable<WikiCompatibilityEntry> ParseEntries(IEnumerable<HtmlNode> 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
- string name = node.Descendants("td").FirstOrDefault()?.InnerText?.Trim();
- 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");
- 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,
- Summary = summary
- };
- }
- }
-
- /// <summary>Get a nullable integer attribute value.</summary>
- /// <param name="node">The HTML node.</param>
- /// <param name="attributeName">The attribute name.</param>
- private int? GetNullableIntAttribute(HtmlNode node, string attributeName)
- {
- string raw = this.GetAttribute(node, attributeName);
- if (raw != null && int.TryParse(raw, out int value))
- return value;
- return null;
- }
-
- /// <summary>Get a strings attribute value.</summary>
- /// <param name="node">The HTML node.</param>
- /// <param name="attributeName">The attribute name.</param>
- private string GetAttribute(HtmlNode node, string attributeName)
- {
- string raw = node.GetAttributeValue(attributeName, null);
- if (raw != null)
- raw = HtmlEntity.DeEntitize(raw);
- return raw;
- }
-
- /// <summary>The response model for the MediaWiki parse API.</summary>
- [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
- [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
- private class ResponseModel
- {
- /// <summary>The parse API results.</summary>
- public ResponseParseModel Parse { get; set; }
- }
-
- /// <summary>The inner response model for the MediaWiki parse API.</summary>
- [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
- [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
- [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
- private class ResponseParseModel
- {
- /// <summary>The parsed text.</summary>
- public IDictionary<string, string> Text { get; set; }
- }
- }
-}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs
deleted file mode 100644
index 8bc66e20..00000000
--- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
-{
- /// <summary>An entry in the mod compatibility list.</summary>
- public class WikiCompatibilityEntry
- {
- /// <summary>The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates).</summary>
- public string[] ID { get; set; }
-
- /// <summary>The mod's display name.</summary>
- public string Name { get; set; }
-
- /// <summary>The mod ID on Nexus.</summary>
- public int? NexusID { get; set; }
-
- /// <summary>The mod ID in the Chucklefish mod repo.</summary>
- public int? ChucklefishID { get; set; }
-
- /// <summary>The GitHub repository in the form 'owner/repo'.</summary>
- public string GitHubRepo { get; set; }
-
- /// <summary>The URL to a non-GitHub source repo.</summary>
- public string CustomSourceUrl { get; set; }
-
- /// <summary>The custom mod page URL (if applicable).</summary>
- public string CustomUrl { get; set; }
-
- /// <summary>The version of the latest unofficial update, if applicable.</summary>
- public ISemanticVersion UnofficialVersion { get; set; }
-
- /// <summary>The compatibility status.</summary>
- public WikiCompatibilityStatus Status { get; set; }
-
- /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatitng.</summary>
- public string Summary { get; set; }
- }
-}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
new file mode 100644
index 00000000..204acd2b
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
@@ -0,0 +1,24 @@
+namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
+{
+ /// <summary>Compatibility info for a mod.</summary>
+ public class WikiCompatibilityInfo
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The compatibility status.</summary>
+ public WikiCompatibilityStatus Status { get; set; }
+
+ /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
+ public string Summary { get; set; }
+
+ /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
+ public string BrokeIn { get; set; }
+
+ /// <summary>The version of the latest unofficial update, if applicable.</summary>
+ public ISemanticVersion UnofficialVersion { get; set; }
+
+ /// <summary>The URL to the latest unofficial update, if applicable.</summary>
+ public string UnofficialUrl { get; set; }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
new file mode 100644
index 00000000..ce8d6c5f
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
@@ -0,0 +1,48 @@
+namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
+{
+ /// <summary>A mod entry in the wiki list.</summary>
+ public class WikiModEntry
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order.</summary>
+ public string[] ID { get; set; }
+
+ /// <summary>The mod's display name. If the mod has multiple names, the first one is the most canonical name.</summary>
+ public string[] Name { get; set; }
+
+ /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
+ public string[] Author { get; set; }
+
+ /// <summary>The mod ID on Nexus.</summary>
+ public int? NexusID { get; set; }
+
+ /// <summary>The mod ID in the Chucklefish mod repo.</summary>
+ public int? ChucklefishID { get; set; }
+
+ /// <summary>The GitHub repository in the form 'owner/repo'.</summary>
+ public string GitHubRepo { get; set; }
+
+ /// <summary>The URL to a non-GitHub source repo.</summary>
+ public string CustomSourceUrl { get; set; }
+
+ /// <summary>The custom mod page URL (if applicable).</summary>
+ public string CustomUrl { get; set; }
+
+ /// <summary>The mod's compatibility with the latest stable version of the game.</summary>
+ public WikiCompatibilityInfo Compatibility { get; set; }
+
+ /// <summary>The mod's compatibility with the latest beta version of the game (if any).</summary>
+ public WikiCompatibilityInfo BetaCompatibility { get; set; }
+
+ /// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="BetaCompatibility"/> should be used for beta versions of SMAPI instead of <see cref="Compatibility"/>.</summary>
+ public bool HasBetaInfo => this.BetaCompatibility != null;
+
+ /// <summary>The human-readable warnings for players about this mod.</summary>
+ public string[] Warnings { get; set; }
+
+ /// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
+ public string Anchor { get; set; }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
new file mode 100644
index 00000000..0d614f28
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
+{
+ /// <summary>Metadata from the wiki's mod compatibility list.</summary>
+ public class WikiModList
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The stable game version.</summary>
+ public string StableVersion { get; set; }
+
+ /// <summary>The beta game version (if any).</summary>
+ public string BetaVersion { get; set; }
+
+ /// <summary>The mods on the wiki.</summary>
+ public WikiModEntry[] Mods { get; set; }
+ }
+}