From 6328574aac5f6cf66ed23b9681a166688638706c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2024 01:13:36 -0500 Subject: [PATCH 01/16] add PathUtilities.CreateSlug and improve IsSlug --- docs/release-notes.md | 6 ++ .../Utilities/PathUtilitiesTests.cs | 70 +++++++++++++++++-- src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 18 ++++- src/SMAPI/Utilities/PathUtilities.cs | 9 +++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5265f1548..bb0632c24 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,12 @@ ← [README](README.md) # Release notes +## Upcoming release +* For mod authors: + * Added `PathUtilities.CreateSlug` to turn an arbitrary string into a safe 'slug' that can be used in special contexts like URLs and file paths. + _For example, `PathUtilities.CreateSlug("some 例子?!/\\~ text")` becomes `"some-例子-text"`._ + * `PathUtilities.IsSlug` is now less strict and allows more Unicode characters. + ## 4.1.7 Released 12 November 2024 for Stardew Valley 1.6.14 or later. diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index c6d323be4..d8b77835e 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -15,7 +15,7 @@ internal class PathUtilitiesTests ** Sample data *********/ /// Sample paths used in unit tests. - public static readonly SamplePath[] SamplePaths = { + public static readonly SamplePath[] SamplePaths = [ // Windows absolute path new( OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley", @@ -136,7 +136,7 @@ internal class PathUtilitiesTests NormalizedOnWindows: @"C:\some\mixed\path\separators", NormalizedOnUnix: @"C:/some/mixed/path/separators" ) - }; + ]; /********* @@ -145,7 +145,7 @@ internal class PathUtilitiesTests /**** ** GetSegments ****/ - [Test(Description = "Assert that PathUtilities.GetSegments splits paths correctly.")] + [Test(Description = $"Assert that {nameof(PathUtilities.GetSegments)} splits paths correctly.")] [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] public void GetSegments(SamplePath path) { @@ -158,7 +158,7 @@ public void GetSegments(SamplePath path) .And.ContainInOrder(segments); } - [Test(Description = "Assert that PathUtilities.GetSegments splits paths correctly when given a limit.")] + [Test(Description = $"Assert that {nameof(PathUtilities.GetSegments)} splits paths correctly when given a limit.")] [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] public void GetSegments_WithLimit(SamplePath path) { @@ -174,7 +174,7 @@ public void GetSegments_WithLimit(SamplePath path) /**** ** NormalizeAssetName ****/ - [Test(Description = "Assert that PathUtilities.NormalizeAssetName normalizes paths correctly.")] + [Test(Description = $"Assert that {nameof(PathUtilities.NormalizeAssetName)} normalizes paths correctly.")] [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] public void NormalizeAssetName(SamplePath path) { @@ -191,7 +191,7 @@ public void NormalizeAssetName(SamplePath path) /**** ** NormalizePath ****/ - [Test(Description = "Assert that PathUtilities.NormalizePath normalizes paths correctly.")] + [Test(Description = $"Assert that {nameof(PathUtilities.NormalizePath)} normalizes paths correctly.")] [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] public void NormalizePath(SamplePath path) { @@ -211,7 +211,7 @@ public void NormalizePath(SamplePath path) /**** ** GetRelativePath ****/ - [Test(Description = "Assert that PathUtilities.GetRelativePath returns the expected values.")] + [Test(Description = $"Assert that {nameof(PathUtilities.GetRelativePath)} returns the expected values.")] #if SMAPI_FOR_WINDOWS [TestCase( @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley", @@ -275,6 +275,62 @@ public string GetRelativePath(string sourceDir, string targetPath) return PathUtilities.GetRelativePath(sourceDir, targetPath); } + /**** + ** IsSlug + ****/ + [Test(Description = $"Assert that {nameof(PathUtilities.IsSlug)} returns the expected values.")] + [TestCase("example", ExpectedResult = true)] + [TestCase("example2", ExpectedResult = true)] + [TestCase("ex-ample", ExpectedResult = true)] + [TestCase("ex_ample", ExpectedResult = true)] + [TestCase("ex.ample", ExpectedResult = true)] + [TestCase("ex-ample---text", ExpectedResult = true)] + [TestCase("eXAMple", ExpectedResult = true)] + [TestCase("example-例子-text", ExpectedResult = true)] + + [TestCase(" example", ExpectedResult = false)] + [TestCase("example ", ExpectedResult = false)] + [TestCase("exa mple", ExpectedResult = false)] + [TestCase("exam!ple", ExpectedResult = false)] + [TestCase("example?", ExpectedResult = false)] + [TestCase("#example", ExpectedResult = false)] + [TestCase("example~", ExpectedResult = false)] + [TestCase("example/", ExpectedResult = false)] + [TestCase("example\\", ExpectedResult = false)] + [TestCase("example|", ExpectedResult = false)] + public bool IsSlug(string input) + { + return PathUtilities.IsSlug(input); + } + + /**** + ** GetSlug + ****/ + [Test(Description = $"Assert that {nameof(PathUtilities.CreateSlug)} returns the expected values.")] + [TestCase("example", ExpectedResult = "example")] + [TestCase("example2", ExpectedResult = "example2")] + [TestCase("ex-ample", ExpectedResult = "ex-ample")] + [TestCase("ex_ample", ExpectedResult = "ex_ample")] + [TestCase("ex.ample", ExpectedResult = "ex.ample")] + [TestCase("ex-ample---text", ExpectedResult = "ex-ample---text")] + [TestCase("eXAMple", ExpectedResult = "eXAMple")] + [TestCase("example-例子-text", ExpectedResult = "example-例子-text")] + + [TestCase(" example", ExpectedResult = "example")] + [TestCase("example ", ExpectedResult = "example-")] + [TestCase("exa mple", ExpectedResult = "exa-mple")] + [TestCase("exam!ple", ExpectedResult = "exam-ple")] + [TestCase("example?", ExpectedResult = "example-")] + [TestCase("#example", ExpectedResult = "example")] + [TestCase("example~", ExpectedResult = "example-")] + [TestCase("example/", ExpectedResult = "example-")] + [TestCase("example\\", ExpectedResult = "example-")] + [TestCase("example|", ExpectedResult = "example-")] + public string CreateSlug(string input) + { + return PathUtilities.CreateSlug(input); + } + /********* ** Private classes diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 94a942d96..4626bdc4a 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -16,6 +16,9 @@ public static class PathUtilities /// The root prefix for a Windows UNC path. private const string WindowsUncRoot = @"\\"; + /// The regex characters that are allowed in a 'slug' that can be used in many different contexts, formatted for use in a [] regex character class. + private const string SlugCharacterClass = @"\p{L}\d\-_\."; // Unicode 'letter' character, digit, dash, underscore, or period + /********* ** Accessors @@ -176,6 +179,19 @@ public static bool IsSafeRelativePath(string? path) && PathUtilities.GetSegments(path).All(segment => segment.Trim() != ".."); } + /// Create a 'slug' containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// The string to represent. + [Pure] +#if NET6_0_OR_GREATER + [return: NotNullIfNotNull("input")] +#endif + public static string? CreateSlug(string? input) + { + return string.IsNullOrWhiteSpace(input) + ? input + : Regex.Replace(input, "[^" + PathUtilities.SlugCharacterClass + "]+", "-").TrimStart('-'); + } + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). /// The string to check. [Pure] @@ -183,6 +199,6 @@ public static bool IsSlug(string? str) { return string.IsNullOrWhiteSpace(str) - || !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); + || !Regex.IsMatch(str, "[^+" + PathUtilities.SlugCharacterClass + "]", RegexOptions.IgnoreCase); } } diff --git a/src/SMAPI/Utilities/PathUtilities.cs b/src/SMAPI/Utilities/PathUtilities.cs index 554c89f64..425ab2839 100644 --- a/src/SMAPI/Utilities/PathUtilities.cs +++ b/src/SMAPI/Utilities/PathUtilities.cs @@ -61,6 +61,15 @@ public static bool IsSafeRelativePath(string? path) return ToolkitPathUtilities.IsSafeRelativePath(path); } + /// Create a 'slug' containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// The string to represent. + [Pure] + [return: NotNullIfNotNull("input")] + public static string? CreateSlug(string? input) + { + return ToolkitPathUtilities.CreateSlug(input); + } + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). /// The string to check. [Pure] From 71f98d62b9f0552dafe4da905dd8773c054137f8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2024 01:13:36 -0500 Subject: [PATCH 02/16] drop support for beta compatibility status This wasn't used in any of the recent game or SMAPI versions. --- docs/technical/web.md | 14 ++--- .../WebApi/ModExtendedMetadataModel.cs | 24 +-------- .../Framework/Clients/Wiki/WikiClient.cs | 29 +--------- .../Framework/Clients/Wiki/WikiModEntry.cs | 14 +---- .../Framework/Clients/Wiki/WikiModList.cs | 12 +---- src/SMAPI.Web/BackgroundService.cs | 2 +- .../Controllers/ModsApiController.cs | 21 +------- src/SMAPI.Web/Controllers/ModsController.cs | 4 +- .../Caching/Wiki/IWikiCacheRepository.cs | 4 +- .../Caching/Wiki/WikiCacheMemoryRepository.cs | 6 +-- .../Framework/Caching/Wiki/WikiMetadata.cs | 25 +-------- src/SMAPI.Web/ViewModels/ModListModel.cs | 12 +---- src/SMAPI.Web/ViewModels/ModModel.cs | 8 +-- src/SMAPI.Web/Views/Mods/Index.cshtml | 23 +------- src/SMAPI.Web/appsettings.json | 1 - src/SMAPI.Web/wwwroot/Content/css/mods.css | 6 --- src/SMAPI.Web/wwwroot/Content/js/mods.js | 53 +++---------------- 17 files changed, 27 insertions(+), 231 deletions(-) diff --git a/docs/technical/web.md b/docs/technical/web.md index 4b99436b7..a4b874bf7 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -228,10 +228,9 @@ The mod ID you specified in the request. The update version recommended by the web API, if any. This is based on some internal rules (e.g. -it won't recommend a prerelease update if the player has a working stable version) and context -(e.g. whether the player is in the game beta channel). Choosing an update version yourself isn't -recommended, but you can set `includeExtendedMetadata: true` and check the `metadata` field if you -really want to do that. +it won't recommend a prerelease update if the player has a working stable version). Choosing an +update version yourself isn't recommended, but you can set `includeExtendedMetadata: true` and +check the `metadata` field if you really want to do that. @@ -268,12 +267,9 @@ field | summary `main` | The primary mod version, if any. This depends on the mod site, but it's typically either the version of the mod itself or of its latest non-optional download. `optional` | The latest optional download version, if any. `unofficial` | The version of the unofficial update defined on the wiki for this mod, if any. -`unofficialForBeta` | Equivalent to `unofficial`, but for beta versions of SMAPI or Stardew Valley. -`hasBetaInfo` | Whether there's an ongoing Stardew Valley or SMAPI beta which may affect update checks. `compatibilityStatus` | The compatibility status for the mod for the stable version of the game, as defined on the wiki, if any. See [possible values](https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs). `compatibilitySummary` | The human-readable summary of the mod's compatibility in HTML format, if any. `brokeIn` | The SMAPI or Stardew Valley version that broke this mod, if any. -`betaCompatibilityStatus`
`betaCompatibilitySummary`
`betaBrokeIn` | Equivalent to the preceding fields, but for beta versions of SMAPI or Stardew Valley. @@ -314,7 +310,6 @@ Example response with `includeExtendedMetadata: true`: "version": "1.10", "url": "https://www.nexusmods.com/stardewvalley/mods/1915" }, - "hasBetaInfo": true, "compatibilityStatus": "Ok", "compatibilitySummary": "✓ use latest version." }, @@ -389,8 +384,7 @@ Initial setup: property name | description ------------------------------- | ----------------- `BackgroundServices:Enabled` | Set to `true` to enable background processes like fetching data from the wiki, or false to disable them. - `Site:BetaEnabled` | Set to `true` to show a separate download button if there's a beta version of SMAPI in its GitHub releases. - `Site:BetaBlurb` | If `Site:BetaEnabled` is true and there's a beta version of SMAPI in its GitHub releases, this is shown on the beta download button as explanatory subtext. + `Site:OtherBlurb` | A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format. `Site:SupporterList` | A list of Patreon supports to credit on the download page. To deploy updates, just [redeploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure). diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 0887c95ef..22fdee264 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -56,9 +56,6 @@ public class ModExtendedMetadataModel /// The latest unofficial version, if newer than and . public ModEntryVersionModel? Unofficial { get; set; } - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. - public ModEntryVersionModel? UnofficialForBeta { get; set; } - /**** ** Stable compatibility ****/ @@ -72,19 +69,6 @@ public class ModExtendedMetadataModel /// The game or SMAPI version which broke this mod, if applicable. public string? BrokeIn { get; set; } - /**** - ** Beta compatibility - ****/ - /// The compatibility status for the Stardew Valley beta (if any). - [JsonConverter(typeof(StringEnumConverter))] - public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } - - /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting. - public string? BetaCompatibilitySummary { get; set; } - - /// The beta game or SMAPI version which broke this mod, if applicable. - public string? BetaBrokeIn { get; set; } - /**** ** Version mappings ****/ @@ -110,14 +94,12 @@ public ModExtendedMetadataModel() { } /// The main version. /// The latest optional version, if newer than . /// The latest unofficial version, if newer than and . - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. - public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial, ModEntryVersionModel? unofficialForBeta) + public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial) { // versions this.Main = main; this.Optional = optional; this.Unofficial = unofficial; - this.UnofficialForBeta = unofficialForBeta; // wiki data if (wiki != null) @@ -137,10 +119,6 @@ public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryV this.CompatibilitySummary = wiki.Compatibility.Summary; this.BrokeIn = wiki.Compatibility.BrokeIn; - this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status; - this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary; - this.BetaBrokeIn = wiki.BetaCompatibility?.BrokeIn; - this.ChangeLocalVersions = wiki.Overrides?.ChangeLocalVersions?.ToString(); this.ChangeRemoteVersions = wiki.Overrides?.ChangeRemoteVersions?.ToString(); this.ChangeUpdateKeys = wiki.Overrides?.ChangeUpdateKeys?.ToString(); diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 8c6681ebb..894051272 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -50,12 +50,6 @@ public async Task FetchModsAsync() 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; - if (betaVersion == stableVersion) - betaVersion = null; - // parse mod data overrides Dictionary overrides = new Dictionary(StringComparer.OrdinalIgnoreCase); { @@ -83,11 +77,7 @@ public async Task FetchModsAsync() } // build model - return new WikiModList( - stableVersion: stableVersion, - betaVersion: betaVersion, - mods: mods - ); + return new WikiModList(mods); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -134,22 +124,6 @@ private IEnumerable ParseModEntries(IEnumerable nodes, I summary: this.GetInnerHtml(node, "mod-summary")?.Trim() ); - // parse beta compatibility - WikiCompatibilityInfo? betaCompatibility = null; - { - WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum(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") - ); - } - } - // find data overrides WikiDataOverrideEntry? overrides = ids .Select(id => overridesById.TryGetValue(id, out overrides) ? overrides : null) @@ -170,7 +144,6 @@ private IEnumerable ParseModEntries(IEnumerable nodes, I customUrl: customUrl, contentPackFor: contentPackFor, compatibility: compatibility, - betaCompatibility: betaCompatibility, warnings: warnings, pullRequestUrl: pullRequestUrl, devNote: devNote, diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 291e31085..8eadee912 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -49,15 +48,6 @@ public class WikiModEntry /// The mod's compatibility with the latest stable version of the game. public WikiCompatibilityInfo Compatibility { get; } - /// The mod's compatibility with the latest beta version of the game (if any). - public WikiCompatibilityInfo? BetaCompatibility { get; } - - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . -#if NET6_0_OR_GREATER - [MemberNotNullWhen(true, nameof(WikiModEntry.BetaCompatibility))] -#endif - public bool HasBetaInfo => this.BetaCompatibility != null; - /// The human-readable warnings for players about this mod. public string[] Warnings { get; } @@ -91,13 +81,12 @@ public class WikiModEntry /// The custom mod page URL (if applicable). /// The name of the mod which loads this content pack, if applicable. /// The mod's compatibility with the latest stable version of the game. - /// The mod's compatibility with the latest beta version of the game (if any). /// The human-readable warnings for players about this mod. /// The URL of the pull request which submits changes for an unofficial update to the author, if any. /// Special notes intended for developers who maintain unofficial updates or submit pull requests. /// The data overrides to apply to the mod's manifest or remote mod page data, if any. /// The link anchor for the mod entry in the wiki compatibility list. - public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, WikiCompatibilityInfo? betaCompatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) + public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) { this.ID = id; this.Name = name; @@ -112,7 +101,6 @@ public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, i this.CustomUrl = customUrl; this.ContentPackFor = contentPackFor; this.Compatibility = compatibility; - this.BetaCompatibility = betaCompatibility; this.Warnings = warnings; this.PullRequestUrl = pullRequestUrl; this.DevNote = devNote; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs index 3329c3005..3df02767f 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs @@ -6,12 +6,6 @@ public class WikiModList /********* ** Accessors *********/ - /// The stable game version. - public string? StableVersion { get; } - - /// The beta game version (if any). - public string? BetaVersion { get; } - /// The mods on the wiki. public WikiModEntry[] Mods { get; } @@ -20,13 +14,9 @@ public class WikiModList ** Public methods *********/ /// Construct an instance. - /// The stable game version. - /// The beta game version (if any). /// The mods on the wiki. - public WikiModList(string? stableVersion, string? betaVersion, WikiModEntry[] mods) + public WikiModList(WikiModEntry[] mods) { - this.StableVersion = stableVersion; - this.BetaVersion = betaVersion; this.Mods = mods; } } diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index bb8692f32..1827d9a95 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -196,7 +196,7 @@ public static async Task UpdateWikiAsync(PerformContext? context) WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); context.WriteLine("Saving data..."); - BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods); + BackgroundService.WikiCache.SaveWikiData(wikiCompatList.Mods); context.WriteLine("Done!"); } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index eeae13bc4..69c13c516 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -155,7 +155,6 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod ModEntryVersionModel? main = null; ModEntryVersionModel? optional = null; ModEntryVersionModel? unofficial = null; - ModEntryVersionModel? unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // validate update key @@ -197,22 +196,6 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}"); - // get unofficial version for beta - if (wikiEntry is { HasBetaInfo: true }) - { - if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) - { - if (wikiEntry.BetaCompatibility.UnofficialVersion != null) - { - unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) - ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}") - : null; - } - else - unofficialForBeta = unofficial; - } - } - // fallback to preview if latest is invalid if (main == null && optional != null) { @@ -241,8 +224,6 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod updates.Add(optional); if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: true)) updates.Add(unofficial); - if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) - updates.Add(unofficialForBeta); // get newest version ModEntryVersionModel? newest = null; @@ -260,7 +241,7 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod // add extended metadata if (includeExtendedMetadata) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta); + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial); // add result result.Errors = errors.ToArray(); diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index b76fbb9b4..cef6aceac 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -54,12 +54,10 @@ public ModListModel FetchData() { // fetch cached data if (!this.Cache.TryGetWikiMetadata(out Cached? metadata)) - return new ModListModel(null, null, Array.Empty(), lastUpdated: DateTimeOffset.UtcNow, isStale: true); + return new ModListModel(Array.Empty(), lastUpdated: DateTimeOffset.UtcNow, isStale: true); // build model return new ModListModel( - stableVersion: metadata.Data.StableVersion, - betaVersion: metadata.Data.BetaVersion, mods: this.Cache .GetWikiMods() .Select(mod => new ModModel(mod.Data)) diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index 0370ac18d..a59e31152 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -20,8 +20,6 @@ internal interface IWikiCacheRepository : ICacheRepository IEnumerable> GetWikiMods(Func? filter = null); /// Save data fetched from the wiki compatibility list. - /// The current stable Stardew Valley version. - /// The current beta Stardew Valley version. /// The mod data. - void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable mods); + void SaveWikiData(IEnumerable mods); } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs index 94819ce89..0ff8c4451 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -42,12 +42,10 @@ public IEnumerable> GetWikiMods(Func? f } /// Save data fetched from the wiki compatibility list. - /// The current stable Stardew Valley version. - /// The current beta Stardew Valley version. /// The mod data. - public void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable mods) + public void SaveWikiData(IEnumerable mods) { - this.Metadata = new Cached(new WikiMetadata(stableVersion, betaVersion)); + this.Metadata = new Cached(new WikiMetadata()); this.Mods = mods.Select(mod => new Cached(mod)).ToArray(); } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs index 5e264ce80..324bd56f5 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs @@ -1,27 +1,4 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki; /// The model for cached wiki metadata. -internal class WikiMetadata -{ - /********* - ** Accessors - *********/ - /// The current stable Stardew Valley version. - public string? StableVersion { get; } - - /// The current beta Stardew Valley version. - public string? BetaVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current stable Stardew Valley version. - /// The current beta Stardew Valley version. - public WikiMetadata(string? stableVersion, string? betaVersion) - { - this.StableVersion = stableVersion; - this.BetaVersion = betaVersion; - } -} +internal class WikiMetadata; diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs index 188de025a..9a6a0f897 100644 --- a/src/SMAPI.Web/ViewModels/ModListModel.cs +++ b/src/SMAPI.Web/ViewModels/ModListModel.cs @@ -10,12 +10,6 @@ public class ModListModel /********* ** Accessors *********/ - /// The current stable version of the game. - public string? StableVersion { get; } - - /// The current beta version of the game (if any). - public string? BetaVersion { get; } - /// The mods to display. public ModModel[] Mods { get; } @@ -33,15 +27,11 @@ public class ModListModel ** Public methods *********/ /// Construct an instance. - /// The current stable version of the game. - /// The current beta version of the game (if any). /// The mods to display. /// When the data was last updated. /// Whether the data hasn't been updated in a while. - public ModListModel(string? stableVersion, string? betaVersion, IEnumerable mods, DateTimeOffset lastUpdated, bool isStale) + public ModListModel(IEnumerable mods, DateTimeOffset lastUpdated, bool isStale) { - this.StableVersion = stableVersion; - this.BetaVersion = betaVersion; this.Mods = mods.ToArray(); this.LastUpdated = lastUpdated; this.IsStale = isStale; diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 197100aa7..ebdb220f3 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -33,9 +33,6 @@ public class ModModel /// The compatibility status for the stable version of the game. public ModCompatibilityModel Compatibility { get; } - /// The compatibility status for the beta version of the game. - public ModCompatibilityModel? BetaCompatibility { get; } - /// Links to the available mod pages. public ModLinkModel[] ModPages { get; } @@ -66,14 +63,13 @@ public class ModModel /// The GitHub repo, if any. /// The URL to the mod's source code, if any. /// The compatibility status for the stable version of the game. - /// The compatibility status for the beta version of the game. /// Links to the available mod pages. /// The human-readable warnings for players about this mod. /// The URL of the pull request which submits changes for an unofficial update to the author, if any. /// Special notes intended for developers who maintain unofficial updates or submit pull requests. /// A unique identifier for the mod that can be used in an anchor URL. [JsonConstructor] - public ModModel(string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModCompatibilityModel betaCompatibility, ModLinkModel[] modPages, string[] warnings, string pullRequestUrl, string devNote, string slug) + public ModModel(string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModLinkModel[] modPages, string[] warnings, string pullRequestUrl, string devNote, string slug) { this.Name = name; this.AlternateNames = alternateNames; @@ -82,7 +78,6 @@ public ModModel(string? name, string alternateNames, string author, string alter this.GitHubRepo = gitHubRepo; this.SourceUrl = sourceUrl; this.Compatibility = compatibility; - this.BetaCompatibility = betaCompatibility; this.ModPages = modPages; this.Warnings = warnings; this.PullRequestUrl = pullRequestUrl; @@ -102,7 +97,6 @@ public ModModel(WikiModEntry entry) this.GitHubRepo = entry.GitHubRepo; this.SourceUrl = this.GetSourceUrl(entry); this.Compatibility = new ModCompatibilityModel(entry.Compatibility); - this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null; this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; this.PullRequestUrl = entry.PullRequestUrl; diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 801e60d94..a9a93b099 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -6,9 +6,6 @@ ViewData["Title"] = "Mod compatibility"; TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated; - - bool hasBeta = Model.BetaVersion != null; - string betaLabel = $"SDV {Model.BetaVersion} only"; } @section Head { @@ -19,8 +16,7 @@ } @@ -41,11 +37,6 @@ else

This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check the troubleshooting guide or ask for help.

The list is updated every few days (you can help update it!). It doesn't include XNB mods (see using XNB mods on the wiki instead) or compatible content packs.

- - @if (hasBeta) - { -

Note: "@betaLabel" lines are for the beta version of Stardew Valley, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions.

- }
@@ -67,12 +58,6 @@ else
{{visibleMainStats.total}} mods shown ({{visibleMainStats.percentCompatible}}% compatible or have a workaround, {{visibleMainStats.percentBroken}}% broken, {{visibleMainStats.percentObsolete}}% obsolete).
- @if (hasBeta) - { -
- @betaLabel: {{visibleBetaStats.total}} mods shown ({{visibleBetaStats.percentCompatible}}% compatible or have a workaround, {{visibleBetaStats.percentBroken}}% broken, {{visibleBetaStats.percentObsolete}}% obsolete). -
- } No matching mods found.
@@ -104,13 +89,9 @@ else - +
-
- @betaLabel: - -
⚠ {{warning}}
source diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 82ecaecab..d8cae83b9 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -16,7 +16,6 @@ }, "Site": { - "BetaEnabled": false, "OtherBlurb": null, "SupporterList": null }, diff --git a/src/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css index 4f6578cb9..62f8aa398 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/mods.css +++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css @@ -9,12 +9,6 @@ max-width: 60em; } -#beta-blurb { - margin-bottom: 2em; - padding: 1em; - border: 3px solid darkgreen; -} - #options { margin-bottom: 1em; } diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js index 945f93efc..a6c3f56f6 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/mods.js +++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js @@ -1,6 +1,6 @@ var smapi = smapi || {}; var app; -smapi.modList = function (mods, enableBeta) { +smapi.modList = function (mods) { // init data var defaultStats = { total: 0, @@ -18,7 +18,6 @@ smapi.modList = function (mods, enableBeta) { mods: mods, showAdvanced: false, visibleMainStats: $.extend({}, defaultStats), - visibleBetaStats: $.extend({}, defaultStats), filters: { source: { value: { @@ -27,7 +26,7 @@ smapi.modList = function (mods, enableBeta) { } }, status: { - label: enableBeta ? "main status" : "status", + label: "status", value: { // note: keys must match status returned by the API ok: { value: true }, @@ -39,10 +38,6 @@ smapi.modList = function (mods, enableBeta) { obsolete: { value: true } } }, - betaStatus: { - label: "beta status", - value: {} // cloned from status field if needed - }, download: { value: { chucklefish: { value: true, label: "Chucklefish" }, @@ -65,17 +60,6 @@ smapi.modList = function (mods, enableBeta) { }); }); - // init beta filters - if (enableBeta) { - var filterGroup = data.filters.betaStatus; - $.extend(true, filterGroup.value, data.filters.status.value); - Object.entries(filterGroup.value).forEach(([filterKey, filter]) => { - filter.id = "beta_" + filter.id; - }); - } - else - delete data.filters.betaStatus; - // init mods for (var i = 0; i < data.mods.length; i++) { var mod = mods[i]; @@ -83,18 +67,10 @@ smapi.modList = function (mods, enableBeta) { // set initial visibility mod.Visible = true; - // set overall compatibility - mod.LatestCompatibility = mod.BetaCompatibility || mod.Compatibility; - // concatenate searchable text mod.SearchableText = [mod.Name, mod.AlternateNames, mod.Author, mod.AlternateAuthors, mod.Compatibility.Summary, mod.BrokeIn]; if (mod.Compatibility.UnofficialVersion) mod.SearchableText.push(mod.Compatibility.UnofficialVersion); - if (mod.BetaCompatibility) { - mod.SearchableText.push(mod.BetaCompatibility.Summary); - if (mod.BetaCompatibility.UnofficialVersion) - mod.SearchableText.push(mod.BetaCompatibility.UnofficialVersion); - } for (var p = 0; p < mod.ModPages; p++) mod.SearchableField.push(mod.ModPages[p].Text); mod.SearchableText = mod.SearchableText.join(" ").toLowerCase(); @@ -128,8 +104,7 @@ smapi.modList = function (mods, enableBeta) { var words = data.search.toLowerCase().split(" "); // apply criteria - var mainStats = data.visibleMainStats = $.extend({}, defaultStats); - var betaStats = data.visibleBetaStats = $.extend({}, defaultStats); + var stats = data.visibleMainStats = $.extend({}, defaultStats); for (var i = 0; i < data.mods.length; i++) { var mod = data.mods[i]; mod.Visible = true; @@ -137,20 +112,15 @@ smapi.modList = function (mods, enableBeta) { // check filters mod.Visible = this.matchesFilters(mod, words); if (mod.Visible) { - mainStats.total++; - betaStats.total++; - - mainStats[this.getCompatibilityGroup(mod.Compatibility.Status)]++; - betaStats[this.getCompatibilityGroup(mod.LatestCompatibility.Status)]++; + stats.total++; + stats[this.getCompatibilityGroup(mod.Compatibility.Status)]++; } } // add aggregate stats - for (let stats of [mainStats, betaStats]) { - stats.percentCompatible = Math.round((stats.compatible + stats.workaround) / stats.total * 100); - stats.percentBroken = Math.round((stats.soon + stats.broken) / stats.total * 100); - stats.percentObsolete = Math.round(stats.abandoned / stats.total * 100); - } + stats.percentCompatible = Math.round((stats.compatible + stats.workaround) / stats.total * 100); + stats.percentBroken = Math.round((stats.soon + stats.broken) / stats.total * 100); + stats.percentObsolete = Math.round(stats.abandoned / stats.total * 100); }, /** @@ -190,13 +160,6 @@ smapi.modList = function (mods, enableBeta) { if (filters.status.value[mainStatus] && !filters.status.value[mainStatus].value) return false; - // check beta status - if (enableBeta) { - var betaStatus = mod.LatestCompatibility.Status; - if (filters.betaStatus.value[betaStatus] && !filters.betaStatus.value[betaStatus].value) - return false; - } - // check download sites var ignoreSites = []; From 7918e661b9091dfa9a6011ea77968ff44f933b1c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2024 01:13:36 -0500 Subject: [PATCH 03/16] fix CurseForge links not shown in compatibility list & drop obsolete wiki fields --- docs/release-notes.md | 3 +++ docs/technical/web.md | 2 -- .../Clients/WebApi/ModExtendedMetadataModel.cs | 4 ---- .../Framework/Clients/Wiki/WikiClient.cs | 4 ---- .../Framework/Clients/Wiki/WikiModEntry.cs | 16 +++------------- src/SMAPI.Web/ViewModels/ModModel.cs | 8 +------- src/SMAPI.Web/Views/Mods/Index.cshtml | 1 - 7 files changed, 7 insertions(+), 31 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index bb0632c24..18e00733f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,9 @@ _For example, `PathUtilities.CreateSlug("some 例子?!/\\~ text")` becomes `"some-例子-text"`._ * `PathUtilities.IsSlug` is now less strict and allows more Unicode characters. +* For the web UI: + * Fixed CurseForge links not shown for mods that have a CurseForge page. + ## 4.1.7 Released 12 November 2024 for Stardew Valley 1.6.14 or later. diff --git a/docs/technical/web.md b/docs/technical/web.md index a4b874bf7..eb3d1ae43 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -259,7 +259,6 @@ field | summary `nexusID` | The mod ID on [Nexus Mods](https://www.nexusmods.com/stardewvalley/), if any. `chucklefishID` | The mod ID in the [Chucklefish mod repo](https://community.playstarbound.com/resources/categories/stardew-valley.22/), if any. `curseForgeID` | The mod project ID on [CurseForge](https://www.curseforge.com/stardewvalley), if any. -`curseForgeKey` | The mod key on [CurseForge](https://www.curseforge.com/stardewvalley), if any. This is used in the mod page URL. `modDropID` | The mod ID on [ModDrop](https://www.moddrop.com/stardew-valley), if any. `gitHubRepo` | The GitHub repository containing the mod code, if any. Specified in the `Owner/Repo` form. `customSourceUrl` | The custom URL to the mod code, if any. This is used for mods which aren't stored in a GitHub repo. @@ -303,7 +302,6 @@ Example response with `includeExtendedMetadata: true`: "name": "Content Patcher", "nexusID": 1915, "curseForgeID": 309243, - "curseForgeKey": "content-patcher", "modDropID": 470174, "gitHubRepo": "Pathoschild/StardewMods", "main": { diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 22fdee264..3c4286034 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -32,9 +32,6 @@ public class ModExtendedMetadataModel /// The mod ID in the CurseForge mod repo. public int? CurseForgeID { get; set; } - /// The mod key in the CurseForge mod repo (used in mod page URLs). - public string? CurseForgeKey { get; set; } - /// The mod ID in the ModDrop mod repo. public int? ModDropID { get; set; } @@ -109,7 +106,6 @@ public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryV this.NexusID = wiki.NexusID; this.ChucklefishID = wiki.ChucklefishID; this.CurseForgeID = wiki.CurseForgeID; - this.CurseForgeKey = wiki.CurseForgeKey; this.ModDropID = wiki.ModDropID; this.GitHubRepo = wiki.GitHubRepo; this.CustomSourceUrl = wiki.CustomSourceUrl; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 894051272..5fceb5d08 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -105,7 +105,6 @@ private IEnumerable ParseModEntries(IEnumerable nodes, I int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id"); - string? curseForgeKey = this.GetAttribute(node, "data-curseforge-key"); int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id"); string? githubRepo = this.GetAttribute(node, "data-github"); string? customSourceUrl = this.GetAttribute(node, "data-custom-source"); @@ -113,7 +112,6 @@ private IEnumerable ParseModEntries(IEnumerable nodes, I string? anchor = this.GetAttribute(node, "id"); string? contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string? devNote = this.GetAttribute(node, "data-dev-note"); - string? pullRequestUrl = this.GetAttribute(node, "data-pr"); // parse stable compatibility WikiCompatibilityInfo compatibility = new( @@ -137,7 +135,6 @@ private IEnumerable ParseModEntries(IEnumerable nodes, I nexusId: nexusID, chucklefishId: chucklefishID, curseForgeId: curseForgeID, - curseForgeKey: curseForgeKey, modDropId: modDropID, githubRepo: githubRepo, customSourceUrl: customSourceUrl, @@ -145,7 +142,6 @@ private IEnumerable ParseModEntries(IEnumerable nodes, I contentPackFor: contentPackFor, compatibility: compatibility, warnings: warnings, - pullRequestUrl: pullRequestUrl, devNote: devNote, overrides: overrides, anchor: anchor diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 8eadee912..14a6d5bb0 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -27,9 +27,6 @@ public class WikiModEntry /// The mod ID in the CurseForge mod repo. public int? CurseForgeID { get; } - /// The mod key in the CurseForge mod repo (used in mod page URLs). - public string? CurseForgeKey { get; } - /// The mod ID in the ModDrop mod repo. public int? ModDropID { get; } @@ -51,9 +48,6 @@ public class WikiModEntry /// The human-readable warnings for players about this mod. public string[] Warnings { get; } - /// The URL of the pull request which submits changes for an unofficial update to the author, if any. - public string? PullRequestUrl { get; } - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string? DevNote { get; } @@ -74,7 +68,6 @@ public class WikiModEntry /// The mod ID on Nexus. /// The mod ID in the Chucklefish mod repo. /// The mod ID in the CurseForge mod repo. - /// The mod ID in the CurseForge mod repo. /// The mod ID in the ModDrop mod repo. /// The GitHub repository in the form 'owner/repo'. /// The URL to a non-GitHub source repo. @@ -82,11 +75,10 @@ public class WikiModEntry /// The name of the mod which loads this content pack, if applicable. /// The mod's compatibility with the latest stable version of the game. /// The human-readable warnings for players about this mod. - /// The URL of the pull request which submits changes for an unofficial update to the author, if any. /// Special notes intended for developers who maintain unofficial updates or submit pull requests. /// The data overrides to apply to the mod's manifest or remote mod page data, if any. /// The link anchor for the mod entry in the wiki compatibility list. - public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) + public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, string[] warnings, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) { this.ID = id; this.Name = name; @@ -94,7 +86,6 @@ public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, i this.NexusID = nexusId; this.ChucklefishID = chucklefishId; this.CurseForgeID = curseForgeId; - this.CurseForgeKey = curseForgeKey; this.ModDropID = modDropId; this.GitHubRepo = githubRepo; this.CustomSourceUrl = customSourceUrl; @@ -102,7 +93,6 @@ public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, i this.ContentPackFor = contentPackFor; this.Compatibility = compatibility; this.Warnings = warnings; - this.PullRequestUrl = pullRequestUrl; this.DevNote = devNote; this.Overrides = overrides; this.Anchor = anchor; @@ -124,10 +114,10 @@ public IEnumerable> GetModPageUrls() anyFound = true; yield return new KeyValuePair(ModSiteKey.ModDrop, $"https://www.moddrop.com/stardew-valley/mod/{this.ModDropID}"); } - if (!string.IsNullOrWhiteSpace(this.CurseForgeKey)) + if (this.CurseForgeID.HasValue) { anyFound = true; - yield return new KeyValuePair(ModSiteKey.CurseForge, $"https://www.curseforge.com/stardewvalley/mods/{this.CurseForgeKey}"); + yield return new KeyValuePair(ModSiteKey.CurseForge, $"https://www.curseforge.com/projects/{this.CurseForgeID}"); } if (this.ChucklefishID.HasValue) { diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index ebdb220f3..008dbeddb 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -39,9 +39,6 @@ public class ModModel /// The human-readable warnings for players about this mod. public string[] Warnings { get; } - /// The URL of the pull request which submits changes for an unofficial update to the author, if any. - public string? PullRequestUrl { get; } - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string? DevNote { get; } @@ -65,11 +62,10 @@ public class ModModel /// The compatibility status for the stable version of the game. /// Links to the available mod pages. /// The human-readable warnings for players about this mod. - /// The URL of the pull request which submits changes for an unofficial update to the author, if any. /// Special notes intended for developers who maintain unofficial updates or submit pull requests. /// A unique identifier for the mod that can be used in an anchor URL. [JsonConstructor] - public ModModel(string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModLinkModel[] modPages, string[] warnings, string pullRequestUrl, string devNote, string slug) + public ModModel(string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModLinkModel[] modPages, string[] warnings, string devNote, string slug) { this.Name = name; this.AlternateNames = alternateNames; @@ -80,7 +76,6 @@ public ModModel(string? name, string alternateNames, string author, string alter this.Compatibility = compatibility; this.ModPages = modPages; this.Warnings = warnings; - this.PullRequestUrl = pullRequestUrl; this.DevNote = devNote; this.Slug = slug; } @@ -99,7 +94,6 @@ public ModModel(WikiModEntry entry) this.Compatibility = new ModCompatibilityModel(entry.Compatibility); this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; - this.PullRequestUrl = entry.PullRequestUrl; this.DevNote = entry.DevNote; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index a9a93b099..91c22d713 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -106,7 +106,6 @@ else # - PR [dev note] From cc8441b9316712bd18ed397eb0dd10c035b21f90 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2024 01:13:37 -0500 Subject: [PATCH 04/16] revamp mod compatibility list to use new GitHub repo The wiki compatibility list was right under the max complexity the wiki can render on a single page; most edits to the page were timing out, and editing it was enough to slow down the entire wiki server. The new GitHub repo combines all the data into one place, uses relevant data type (e.g. numbers for mod IDs), adds a validated JSON schema for the data format, and adds a review step to prevent potential abuse of the unofficial update alert feature. This rewrite also... - fixes CurseForge links not shown (since the CurseForgeKey field on the wiki was obsolete); - drops beta support (since it wasn't used for the more recent game updates); - drops obsolete fields that no longer exist in the data (e.g. pull request URL). --- docs/release-notes.md | 1 + .../WebApi/ModExtendedMetadataModel.cs | 44 +- .../Clients/Wiki/ChangeDescriptor.cs | 68 +-- .../DataModels/RawCompatibilityList.cs | 11 + .../Internal/DataModels/RawModDataOverride.cs | 14 + .../Wiki/Internal/DataModels/RawModEntry.cs | 77 ++++ .../DataModels/RawModUnofficialUpdate.cs | 11 + .../Wiki/Internal/SetResponseTypeFilter.cs | 18 + .../Framework/Clients/Wiki/WikiClient.cs | 408 ++++++++---------- .../Clients/Wiki/WikiCompatibilityInfo.cs | 15 +- .../Clients/Wiki/WikiCompatibilityStatus.cs | 2 +- .../Clients/Wiki/WikiDataOverrideEntry.cs | 2 +- .../Framework/Clients/Wiki/WikiModEntry.cs | 14 +- .../Framework/Clients/Wiki/WikiModList.cs | 22 - ...ndCompatibilityListAnchorLinksExtension.cs | 28 ++ ...patibilityListAnchorLinksInlineRenderer.cs | 48 +++ src/SMAPI.Toolkit/ModToolkit.cs | 6 +- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 1 + src/SMAPI.Web/BackgroundService.cs | 28 +- .../Controllers/ModsApiController.cs | 46 +- src/SMAPI.Web/Controllers/ModsController.cs | 12 +- .../Caching/Wiki/IWikiCacheRepository.cs | 16 +- .../Caching/Wiki/WikiCacheMemoryRepository.cs | 31 +- .../Framework/Caching/Wiki/WikiMetadata.cs | 4 +- .../ModCompatibilityListConfig.cs | 2 +- src/SMAPI.Web/Startup.cs | 2 +- .../ViewModels/ModCompatibilityModel.cs | 4 +- src/SMAPI.Web/ViewModels/ModModel.cs | 6 +- src/SMAPI.Web/Views/Mods/Index.cshtml | 6 +- 29 files changed, 547 insertions(+), 400 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs delete mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs create mode 100644 src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksExtension.cs create mode 100644 src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 18e00733f..f182c6f71 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * `PathUtilities.IsSlug` is now less strict and allows more Unicode characters. * For the web UI: + * Revamped how the mod compatibility list works to simplify maintenance. * Fixed CurseForge links not shown for mods that have a CurseForge page. ## 4.1.7 diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 3c4286034..565c20066 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -58,7 +58,7 @@ public class ModExtendedMetadataModel ****/ /// The compatibility status. [JsonConverter(typeof(StringEnumConverter))] - public WikiCompatibilityStatus? CompatibilityStatus { get; set; } + public ModCompatibilityStatus? CompatibilityStatus { get; set; } /// The human-readable summary of the compatibility status or workaround, without HTML formatting. public string? CompatibilitySummary { get; set; } @@ -86,38 +86,38 @@ public class ModExtendedMetadataModel public ModExtendedMetadataModel() { } /// Construct an instance. - /// The mod metadata from the wiki (if available). + /// The mod metadata from the mod compatibility list (if available). /// The mod metadata from SMAPI's internal DB (if available). /// The main version. /// The latest optional version, if newer than . /// The latest unofficial version, if newer than and . - public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial) + public ModExtendedMetadataModel(ModCompatibilityEntry? compatibility, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial) { // versions this.Main = main; this.Optional = optional; this.Unofficial = unofficial; - // wiki data - if (wiki != null) + // compatibility list data + if (compatibility != null) { - this.ID = wiki.ID; - this.Name = wiki.Name.FirstOrDefault(); - this.NexusID = wiki.NexusID; - this.ChucklefishID = wiki.ChucklefishID; - this.CurseForgeID = wiki.CurseForgeID; - this.ModDropID = wiki.ModDropID; - this.GitHubRepo = wiki.GitHubRepo; - this.CustomSourceUrl = wiki.CustomSourceUrl; - this.CustomUrl = wiki.CustomUrl; - - this.CompatibilityStatus = wiki.Compatibility.Status; - this.CompatibilitySummary = wiki.Compatibility.Summary; - this.BrokeIn = wiki.Compatibility.BrokeIn; - - this.ChangeLocalVersions = wiki.Overrides?.ChangeLocalVersions?.ToString(); - this.ChangeRemoteVersions = wiki.Overrides?.ChangeRemoteVersions?.ToString(); - this.ChangeUpdateKeys = wiki.Overrides?.ChangeUpdateKeys?.ToString(); + this.ID = compatibility.ID; + this.Name = compatibility.Name.FirstOrDefault(); + this.NexusID = compatibility.NexusID; + this.ChucklefishID = compatibility.ChucklefishID; + this.CurseForgeID = compatibility.CurseForgeID; + this.ModDropID = compatibility.ModDropID; + this.GitHubRepo = compatibility.GitHubRepo; + this.CustomSourceUrl = compatibility.CustomSourceUrl; + this.CustomUrl = compatibility.CustomUrl; + + this.CompatibilityStatus = compatibility.Compatibility.Status; + this.CompatibilitySummary = compatibility.Compatibility.Summary; + this.BrokeIn = compatibility.Compatibility.BrokeIn; + + this.ChangeLocalVersions = compatibility.Overrides?.ChangeLocalVersions?.ToString(); + this.ChangeRemoteVersions = compatibility.Overrides?.ChangeRemoteVersions?.ToString(); + this.ChangeUpdateKeys = compatibility.Overrides?.ChangeUpdateKeys?.ToString(); } // internal DB data diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs index 5cfcb5ce2..b1efc6b7a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -13,16 +12,16 @@ public class ChangeDescriptor ** Accessors *********/ /// The values to add to the field. - public ISet Add { get; } + public ISet Add { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); /// The values to remove from the field. - public ISet Remove { get; } + public ISet Remove { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); /// The values to replace in the field, if matched. - public IReadOnlyDictionary Replace { get; } + public IDictionary Replace { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// Whether the change descriptor would make any changes. - public bool HasChanges { get; } + public bool HasChanges => this.Add.Count > 0 || this.Remove.Count > 0 || this.Replace.Count > 0; /// Format a raw value into a normalized form. public Func FormatValue { get; } @@ -32,19 +31,33 @@ public class ChangeDescriptor ** Public methods *********/ /// Construct an instance. - /// The values to add to the field. - /// The values to remove from the field. - /// The values to replace in the field, if matched. /// Format a raw value into a normalized form. - public ChangeDescriptor(ISet add, ISet remove, IReadOnlyDictionary replace, Func formatValue) + public ChangeDescriptor(Func formatValue) { - this.Add = add; - this.Remove = remove; - this.Replace = replace; - this.HasChanges = add.Any() || remove.Any() || replace.Any(); this.FormatValue = formatValue; } + /// Add a change to this descriptor. + /// The specific value to replace with the `to` field. For a version number, this must match the exact formatting before the version is parsed. + /// The value to use instead of the `from` value. + public void AddChange(string? from, string? to) + { + from = from != null + ? this.FormatValue(from) + : null; + + to = to != null + ? this.FormatValue(to) + : null; + + if (from != null && to != null) + this.Replace[from] = to; + else if (from != null) + this.Remove.Add(from); + else if (to != null) + this.Add.Add(to); + } + /// Apply the change descriptors to a comma-delimited field. /// The raw field text. /// Returns the modified field. @@ -130,11 +143,7 @@ public override string ToString() /// Format a raw value into a normalized form if needed. public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func? formatValue = null) { - // init - formatValue ??= p => p; - var add = new HashSet(StringComparer.OrdinalIgnoreCase); - var remove = new HashSet(StringComparer.OrdinalIgnoreCase); - var replace = new Dictionary(StringComparer.OrdinalIgnoreCase); + var parsed = new ChangeDescriptor(formatValue ?? (p => p)); // parse each change in the descriptor if (!string.IsNullOrWhiteSpace(descriptor)) @@ -151,8 +160,8 @@ public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Fu if (entry.Contains('→')) { string[] parts = entry.Split(new[] { '→' }, 2); - string oldValue = formatValue(parts[0].Trim()); - string newValue = formatValue(parts[1].Trim()); + string oldValue = parts[0].Trim(); + string newValue = parts[1].Trim(); if (oldValue == string.Empty) { @@ -166,36 +175,31 @@ public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Fu continue; } - replace[oldValue] = newValue; + parsed.AddChange(oldValue, newValue); } // else as remove else if (entry.StartsWith("-")) { - entry = formatValue(entry.Substring(1).Trim()); - remove.Add(entry); + entry = entry.Substring(1).Trim(); + parsed.AddChange(entry, null); } // else as add else { if (entry.StartsWith("+")) - entry = formatValue(entry.Substring(1).Trim()); - add.Add(entry); + entry = entry.Substring(1).Trim(); + parsed.AddChange(null, entry); } } errors = rawErrors.ToArray(); } else - errors = Array.Empty(); + errors = []; // build model - return new ChangeDescriptor( - add: add, - remove: remove, - replace: new ReadOnlyDictionary(replace), - formatValue: formatValue - ); + return parsed; } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs new file mode 100644 index 000000000..4dd5826fd --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs @@ -0,0 +1,11 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; + +/// The main data model for the raw compatibility data. +internal class RawCompatibilityList +{ + /// The compatibility data for C# SMAPI mods. + public RawModEntry[]? Mods { get; set; } + + /// The compatibility data for broken content packs only. This allows providing workarounds and unofficial updates for content packs. However, compatible content packs shouldn't be listed here. + public RawModEntry[]? BrokenContentPacks { get; set; } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs new file mode 100644 index 000000000..bee41d284 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs @@ -0,0 +1,14 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; + +/// As part of , a data override to apply to the mod's manifest or remote mod page data. +internal class RawModDataOverride +{ + /// The data type to override. + public string? Type { get; set; } + + /// The specific value to replace with the `to` field. For a version number, this must match the exact formatting before the version is parsed. + public string? From { get; set; } + + /// The value to use instead of the `from` value. + public string? To { get; set; } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs new file mode 100644 index 000000000..d6b85b925 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs @@ -0,0 +1,77 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; + +/// The compatibility metadata for a mod in the raw data. +internal class RawModEntry +{ + /********* + ** Properties + *********/ + /**** + ** Main fields + ****/ + /// The normalised display name for the mod. If the mod has alternate names, list them comma-separated after the main name. + public string? Name { get; set; } + + /// The normalized display name for the author. If the author has alternate names, list them comma-separated after the main name. + public string? Author { get; set; } + + /// The unique mod ID, as listed in its manifest.json file. If the mod has alternate or former IDs, list them comma-separated after the main one (ideally in latest to oldest order). For very old mods with no ID, use 'none' to disable validation checks. + public string? Id { get; set; } + + /// The mod's unique ID on Nexus, or null if it has none. This is the number in the mod page's URL. + public int? Nexus { get; set; } + + /// The mod's GitHub repository in the form owner/repo, or null if it has none. + public string? GitHub { get; set; } + + /**** + ** Secondary fields + ****/ + /// The mod's unique ID in the legacy Chucklefish mod repository (if any). This is the value shown in the mod page's URL. + public int? Chucklefish { get; set; } + + /// The mod's unique ID on CurseForge (if any). This is the value shown on the mod page next to "Project ID". + public int? Curse { get; set; } + + /// The mod's unique ID on ModDrop (if any). This is the value shown in the mod page's URL. + public int? ModDrop { get; set; } + + /// The arbitrary mod URL, if the mod isn't on a mod site supported by more specific fields like `nexus`. This should be avoided if possible, since this makes cross-referencing much more difficult. + public string? Url { get; set; } + + /// An arbitrary source code URL, if not on GitHub. Avoid if possible, since this makes cross-referencing more difficult. + public string? Source { get; set; } + + /// Custom text indicating compatibility issues with the mod (e.g. not compatible with Linux/Mac). + public string[]? Warnings { get; set; } + + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string? DeveloperNotes { get; set; } + + /**** + ** Compatibility + ****/ + /// The mod's compatibility with the latest versions of Stardew Valley and SMAPI. + public string? Status { get; set; } + + /// A custom description of the mod's compatibility, in Markdown format. This should be left blank if it's covered by more specific fields like `brokeIn` or `unofficialUpdate`. + public string? Summary { get; set; } + + /// The SMAPI, Stardew Valley, or other release which broke this mod if applicable. This should include both the name and version, like 'Stardew Valley 1.6.9'. + public string? BrokeIn { get; set; } + + /// The unofficial update which fixes compatibility with the latest Stardew Valley and SMAPI versions. + public RawModUnofficialUpdate? UnofficialUpdate { get; set; } + + /**** + ** Content packs only + ****/ + /// The unique ID of the framework mod for which this is a content pack. + public string? ContentPackFor { get; set; } + + /**** + ** Data overrides + ****/ + /// The data overrides to apply to the mod's manifest or remote mod page data, if any. + public RawModDataOverride[]? Overrides { get; set; } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs new file mode 100644 index 000000000..7e2b64e2a --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs @@ -0,0 +1,11 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; + +/// As part of , an unofficial update which fixes compatibility with the latest Stardew Valley and SMAPI versions. +internal class RawModUnofficialUpdate +{ + /// The version of the unofficial update. + public string? Version { get; set; } + + /// The URL to the unofficial update. + public string? Url { get; set; } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs new file mode 100644 index 000000000..2c8119223 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs @@ -0,0 +1,18 @@ +using System.Net.Http.Headers; +using Pathoschild.Http.Client; +using Pathoschild.Http.Client.Extensibility; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal; + +/// An HTTP filter which sets the content type for all responses received to text/json. +internal class ForceJsonResponseTypeFilter : IHttpFilter +{ + /// + public void OnRequest(IRequest request) { } + + /// + public void OnResponse(IResponse response, bool httpErrorAsException) + { + response.Message.Content.Headers.ContentType = new MediaTypeHeaderValue("text/json"); + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 5fceb5d08..2c2ede2fa 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -1,17 +1,17 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Net; using System.Threading.Tasks; -using HtmlAgilityPack; +using Markdig; using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; +using StardewModdingAPI.Toolkit.Framework.MarkdownExtensions; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; -/// An HTTP client for fetching mod metadata from the wiki. -public class WikiClient : IDisposable +/// An HTTP client for fetching data from the mod compatibility repo. +public class CompatibilityRepoClient : IDisposable { /********* ** Fields @@ -19,65 +19,37 @@ public class WikiClient : IDisposable /// The underlying HTTP client. private readonly IClient Client; + /// The Markdown pipeline with which to format Markdown summaries. + private readonly MarkdownPipeline MarkdownPipeline; + /********* ** Public methods *********/ /// Construct an instance. - /// The user agent for the wiki API. - /// The base URL for the wiki API. - public WikiClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") + /// The full URL of the JSON file to fetch. + /// The user agent for the API client. + public CompatibilityRepoClient(string userAgent, string fetchUrl = "https://raw.githubusercontent.com/Pathoschild/SmapiCompatibilityList/refs/heads/develop/data/data.jsonc") { - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.Client = new FluentClient(fetchUrl).SetUserAgent(userAgent); + this.MarkdownPipeline = new MarkdownPipelineBuilder() + .Use(new ExpandCompatibilityListAnchorLinksExtension()) + .Build(); } /// Fetch mods from the compatibility list. - public async Task FetchModsAsync() + public async Task FetchModsAsync() { - // fetch HTML - ResponseModel response = await this.Client - .GetAsync("") - .WithArguments(new - { - action = "parse", - page = "Modding:Mod_compatibility", - format = "json" - }) - .As(); - string html = response.Parse.Text["*"]; - - // parse HTML - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - // parse mod data overrides - Dictionary overrides = new Dictionary(StringComparer.OrdinalIgnoreCase); - { - HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("//table[@id='mod-overrides-list']//tr[@class='mod']"); - if (modNodes == null) - throw new InvalidOperationException("Can't parse wiki compatibility list, no mod data overrides section found."); - - foreach (WikiDataOverrideEntry entry in this.ParseOverrideEntries(modNodes)) - { - if (entry.Ids.Any() != true || !entry.HasChanges) - continue; - - foreach (string id in entry.Ids) - overrides[id] = entry; - } - } - - // parse mod entries - WikiModEntry[] mods; - { - 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."); - mods = this.ParseModEntries(modNodes, overrides).ToArray(); - } - - // build model - return new WikiModList(mods); + RawCompatibilityList response = await this.Client + .GetAsync(null) + .WithFilter(new ForceJsonResponseTypeFilter()) + .As(); + + return + (response.Mods ?? Array.Empty()) + .Concat(response.BrokenContentPacks ?? Array.Empty()) + .Select(this.ParseModEntry) + .ToArray(); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -86,204 +58,186 @@ public void Dispose() this.Client.Dispose(); } - /********* ** Private methods *********/ - /// Parse valid mod compatibility entries. - /// The HTML compatibility entries. - /// The mod data overrides to apply, if any. - private IEnumerable ParseModEntries(IEnumerable nodes, IDictionary overridesById) + /// Parse a mod compatibility entry. + /// The HTML compatibility entries. + private ModCompatibilityEntry ParseModEntry(RawModEntry rawModEntry) { - 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"); - int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id"); - int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-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"); - string? contentPackFor = this.GetAttribute(node, "data-content-pack-for"); - string? devNote = this.GetAttribute(node, "data-dev-note"); - - // parse stable compatibility - WikiCompatibilityInfo compatibility = new( - status: this.GetAttributeAsEnum(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() - ); - - // find data overrides - WikiDataOverrideEntry? overrides = ids - .Select(id => overridesById.TryGetValue(id, out overrides) ? overrides : null) - .FirstOrDefault(p => p != null); + // parse main fields + string[] modIds = this.GetCsv(rawModEntry.Id); + string[] modNames = this.GetCsv(rawModEntry.Name); + string[] authorNames = this.GetCsv(rawModEntry.Author); - // yield model - yield return new WikiModEntry( - id: ids, - name: names, - author: authors, - nexusId: nexusID, - chucklefishId: chucklefishID, - curseForgeId: curseForgeID, - modDropId: modDropID, - githubRepo: githubRepo, - customSourceUrl: customSourceUrl, - customUrl: customUrl, - contentPackFor: contentPackFor, - compatibility: compatibility, - warnings: warnings, - devNote: devNote, - overrides: overrides, - anchor: anchor - ); + // parse status + if (!Enum.TryParse(rawModEntry.Status, true, out ModCompatibilityStatus status)) + { + if (rawModEntry.UnofficialUpdate != null) + status = ModCompatibilityStatus.Unofficial; + else if (rawModEntry.BrokeIn != null) + status = ModCompatibilityStatus.Broken; + else + status = ModCompatibilityStatus.Ok; } - } - /// Parse valid mod data override entries. - /// The HTML mod data override entries. - private IEnumerable ParseOverrideEntries(IEnumerable nodes) - { - foreach (HtmlNode node in nodes) + // parse summary + bool hasSource = rawModEntry.GitHub != null || rawModEntry.Source != null; + char summaryIcon = status switch + { + ModCompatibilityStatus.Unofficial or ModCompatibilityStatus.Workaround => '⚠', + ModCompatibilityStatus.Broken when hasSource => '↻', + ModCompatibilityStatus.Broken or ModCompatibilityStatus.Obsolete or ModCompatibilityStatus.Abandoned => '✖', + _ => '✓' + }; + string? summary = rawModEntry.Summary; + bool hasMarkdown = summary != null; + if (summary is null) { - yield return new WikiDataOverrideEntry + switch (status) { - Ids = this.GetAttributeAsCsv(node, "data-id"), - ChangeLocalVersions = this.GetAttributeAsChangeDescriptor(node, "data-local-version", - raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw - ), - ChangeRemoteVersions = this.GetAttributeAsChangeDescriptor(node, "data-remote-version", - raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw - ), - - ChangeUpdateKeys = this.GetAttributeAsChangeDescriptor(node, "data-update-keys", - raw => UpdateKey.TryParse(raw, out UpdateKey key) ? key.ToString() : raw - ) - }; + case ModCompatibilityStatus.Ok: + summary = "use latest version."; + break; + + case ModCompatibilityStatus.Optional: + summary = "use optional download."; + break; + + case ModCompatibilityStatus.Unofficial: + summary = $"broken, use [unofficial version]({rawModEntry.UnofficialUpdate?.Url})"; + if (rawModEntry.UnofficialUpdate?.Version != null) + summary += $" ({rawModEntry.UnofficialUpdate.Version})"; + summary += '.'; + hasMarkdown = true; + break; + + case ModCompatibilityStatus.Workaround: + summary = "broken [**error:** should specify summary]."; + hasMarkdown = true; + break; + + case ModCompatibilityStatus.Broken: + summary = hasSource + ? "broken, not updated yet." + : "broken, not open-source."; + break; + + case ModCompatibilityStatus.Obsolete: + summary = "remove this mod (obsolete)."; + break; + + case ModCompatibilityStatus.Abandoned: + summary = "remove this mod (no longer maintained)."; + break; + + default: + summary = $"[**error:** unknown status '{status}'.]"; + break; + } } - } + summary = $"{summaryIcon} {summary}"; - /// Get an attribute value. - /// The element whose attributes to read. - /// The attribute name. - private string? GetAttribute(HtmlNode element, string name) - { - string value = element.GetAttributeValue(name, null); - if (string.IsNullOrWhiteSpace(value)) - return null; + // get HTML summary + string? htmlSummary = null; + if (hasMarkdown) + { + htmlSummary = this.ToInlineHtml(summary); + if (htmlSummary == summary) + htmlSummary = null; + } - return WebUtility.HtmlDecode(value); + // build model + return new ModCompatibilityEntry( + id: modIds, + name: modNames, + author: authorNames, + nexusId: rawModEntry.Nexus, + chucklefishId: rawModEntry.Chucklefish, + curseForgeId: rawModEntry.Curse, + modDropId: rawModEntry.ModDrop, + githubRepo: rawModEntry.GitHub, + customSourceUrl: rawModEntry.Source, + customUrl: rawModEntry.Url, + contentPackFor: rawModEntry.ContentPackFor, + compatibility: new ModCompatibilityInfo( + status: status, + summary: summary, + htmlSummary: htmlSummary, + brokeIn: rawModEntry.BrokeIn, + unofficialVersion: this.GetSemanticVersion(rawModEntry.UnofficialUpdate?.Version), + unofficialUrl: rawModEntry.UnofficialUpdate?.Url + ), + warnings: rawModEntry.Warnings ?? Array.Empty(), + devNote: rawModEntry.DeveloperNotes, + overrides: this.ParseOverrideEntries(modIds, rawModEntry.Overrides), + anchor: PathUtilities.CreateSlug(modIds.FirstOrDefault())?.ToLower() + ); } - /// Get an attribute value and parse it as a change descriptor. - /// The element whose attributes to read. - /// The attribute name. - /// Format an raw entry value when applying changes. - private ChangeDescriptor? GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func formatValue) + /// Get the inline HTML produced by a Markdown string. + /// The Markdown to parse. + private string ToInlineHtml(string markdown) { - string? raw = this.GetAttribute(element, name); - return raw != null - ? ChangeDescriptor.Parse(raw, out _, formatValue) - : null; - } + string html = Markdown.ToHtml(markdown, this.MarkdownPipeline); - /// Get an attribute value and parse it as a comma-delimited list of strings. - /// The element whose attributes to read. - /// The attribute name. - 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() - : Array.Empty(); - } + // Markdown wraps all content with

, and there's no non-hacky way to disable it. + // We need to strip them since the content is shown inline. + html = html.Trim(); + if (html.StartsWith("

", StringComparison.OrdinalIgnoreCase) && html.EndsWith("

", StringComparison.OrdinalIgnoreCase) && html.IndexOf("

", 3, StringComparison.OrdinalIgnoreCase) == -1) + html = html.Substring(3, html.Length - 7); - ///

Get an attribute value and parse it as an enum value. - /// The enum type. - /// The element whose attributes to read. - /// The attribute name. - private TEnum? GetAttributeAsEnum(HtmlNode element, string name) where TEnum : struct - { - string? raw = this.GetAttribute(element, name); - if (raw == null) - return null; - if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value)) - throw new InvalidOperationException($"Unknown {typeof(TEnum).Name} value '{raw}' when parsing compatibility list."); - return value; + return html; } - /// Get an attribute value and parse it as a semantic version. - /// The element whose attributes to read. - /// The attribute name. - private ISemanticVersion? GetAttributeAsSemanticVersion(HtmlNode element, string name) + /// Parse valid mod data override entries. + /// The mod's unique IDs. + /// The raw data override entries to parse. + private ModDataOverrideEntry? ParseOverrideEntries(string[] modIds, RawModDataOverride[]? overrides) { - string? raw = this.GetAttribute(element, name); - return SemanticVersion.TryParse(raw, out ISemanticVersion? version) - ? version - : null; - } + if (overrides?.Length is not > 0) + return null; - /// Get an attribute value and parse it as a nullable int. - /// The element whose attributes to read. - /// The attribute name. - 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; - } + ModDataOverrideEntry parsed = new() { Ids = modIds }; + foreach (RawModDataOverride @override in overrides) + { + switch (@override.Type?.ToLower()) + { + case "updatekey": + parsed.ChangeUpdateKeys ??= new(raw => raw); + parsed.ChangeUpdateKeys.AddChange(@override.From, @override.To); + break; + + case "localversion": + parsed.ChangeLocalVersions ??= new(raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw); + parsed.ChangeLocalVersions.AddChange(@override.From, @override.To); + break; + + case "remoteversion": + parsed.ChangeRemoteVersions ??= new(raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw); + parsed.ChangeRemoteVersions.AddChange(@override.From, @override.To); + break; + } + } - /// Get the text of an element with the given class name. - /// The metadata container. - /// The field name. - private string? GetInnerHtml(HtmlNode container, string className) - { - return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml; + return parsed; } - /// The response model for the MediaWiki parse API. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialization.")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialization.")] - private class ResponseModel + /// Parse a raw value as a comma-delimited list of strings. + /// The raw value to parse. + private string[] GetCsv(string? rawValue) { - /********* - ** Accessors - *********/ - /// The parse API results. - public ResponseParseModel Parse { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The parse API results. - public ResponseModel(ResponseParseModel parse) - { - this.Parse = parse; - } + return !string.IsNullOrWhiteSpace(rawValue) + ? rawValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() + : Array.Empty(); } - /// The inner response model for the MediaWiki parse API. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialization.")] - [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local", Justification = "Used via JSON deserialization.")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialization.")] - private class ResponseParseModel + /// Get a raw value as a semantic version. + /// The raw value to parse. + private ISemanticVersion? GetSemanticVersion(string? rawValue) { - /********* - ** Accessors - *********/ - /// The parsed text. - public IDictionary Text { get; } = new Dictionary(); + return SemanticVersion.TryParse(rawValue, out ISemanticVersion? version) + ? version + : null; } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs index 5a2bd4436..f8ad2f691 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -1,17 +1,20 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; /// Compatibility info for a mod. -public class WikiCompatibilityInfo +public class ModCompatibilityInfo { /********* ** Accessors *********/ /// The compatibility status. - public WikiCompatibilityStatus Status { get; } + public ModCompatibilityStatus Status { get; } - /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + /// The human-readable summary of the compatibility status or workaround, in Markdown format. public string? Summary { get; } + /// An HTML version of , if different. + public string? HtmlSummary { get; } + /// The game or SMAPI version which broke this mod, if applicable. public string? BrokeIn { get; } @@ -27,14 +30,16 @@ public class WikiCompatibilityInfo *********/ /// Construct an instance. /// The compatibility status. - /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + /// The human-readable summary of the compatibility status or workaround, in Markdown format. + /// An HTML version of , if different. /// The game or SMAPI version which broke this mod, if applicable. /// The version of the latest unofficial update, if applicable. /// The URL to the latest unofficial update, if applicable. - public WikiCompatibilityInfo(WikiCompatibilityStatus status, string? summary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl) + public ModCompatibilityInfo(ModCompatibilityStatus status, string? summary, string? htmlSummary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl) { this.Status = status; this.Summary = summary; + this.HtmlSummary = htmlSummary; this.BrokeIn = brokeIn; this.UnofficialVersion = unofficialVersion; this.UnofficialUrl = unofficialUrl; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs index e30ee3ac7..96b8c4a30 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; /// The compatibility status for a mod. -public enum WikiCompatibilityStatus +public enum ModCompatibilityStatus { /// The status is unknown. Unknown, diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs index e56b4bcbe..1e35005bc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; /// The data overrides to apply to matching mods. -public class WikiDataOverrideEntry +public class ModDataOverrideEntry { /********* ** Accessors diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 14a6d5bb0..460824540 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -3,8 +3,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; -/// A mod entry in the wiki list. -public class WikiModEntry +/// A mod entry in the compatibility list. +public class ModCompatibilityEntry { /********* ** Accessors @@ -43,7 +43,7 @@ public class WikiModEntry public string? ContentPackFor { get; } /// The mod's compatibility with the latest stable version of the game. - public WikiCompatibilityInfo Compatibility { get; } + public ModCompatibilityInfo Compatibility { get; } /// The human-readable warnings for players about this mod. public string[] Warnings { get; } @@ -52,9 +52,9 @@ public class WikiModEntry public string? DevNote { get; } /// The data overrides to apply to the mod's manifest or remote mod page data, if any. - public WikiDataOverrideEntry? Overrides { get; } + public ModDataOverrideEntry? Overrides { get; } - /// The link anchor for the mod entry in the wiki compatibility list. + /// The link anchor for the mod entry in the compatibility list. public string? Anchor { get; } @@ -77,8 +77,8 @@ public class WikiModEntry /// The human-readable warnings for players about this mod. /// Special notes intended for developers who maintain unofficial updates or submit pull requests. /// The data overrides to apply to the mod's manifest or remote mod page data, if any. - /// The link anchor for the mod entry in the wiki compatibility list. - public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, string[] warnings, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) + /// The link anchor for the mod entry in the compatibility list. + public ModCompatibilityEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, ModCompatibilityInfo compatibility, string[] warnings, string? devNote, ModDataOverrideEntry? overrides, string? anchor) { this.ID = id; this.Name = name; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs deleted file mode 100644 index 3df02767f..000000000 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; - -/// Metadata from the wiki's mod compatibility list. -public class WikiModList -{ - /********* - ** Accessors - *********/ - /// The mods on the wiki. - public WikiModEntry[] Mods { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mods on the wiki. - public WikiModList(WikiModEntry[] mods) - { - this.Mods = mods; - } -} diff --git a/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksExtension.cs b/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksExtension.cs new file mode 100644 index 000000000..3c4622614 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksExtension.cs @@ -0,0 +1,28 @@ +using Markdig; +using Markdig.Renderers; +using Markdig.Renderers.Html.Inlines; + +namespace StardewModdingAPI.Toolkit.Framework.MarkdownExtensions; + +/// A Markdown extension which fills in empty anchor links on the compatibility list based on the link text. +public class ExpandCompatibilityListAnchorLinksExtension : IMarkdownExtension +{ + /********* + ** Public methods + *********/ + /// + public void Setup(MarkdownPipelineBuilder pipeline) { } + + /// + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is TextRendererBase htmlRenderer) + { + LinkInlineRenderer? existingRenderer = htmlRenderer.ObjectRenderers.FindExact(); + if (existingRenderer != null) + htmlRenderer.ObjectRenderers.Remove(existingRenderer); + + htmlRenderer.ObjectRenderers.Add(new ExpandCompatibilityListAnchorLinksInlineRenderer()); + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs b/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs new file mode 100644 index 000000000..2c86fe6a0 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs @@ -0,0 +1,48 @@ +using System.Text; +using Markdig.Renderers; +using Markdig.Renderers.Html.Inlines; +using Markdig.Syntax.Inlines; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework.MarkdownExtensions; + +/// A Markdown link renderer which fills in empty anchor links on the compatibility list based on the link text. +public class ExpandCompatibilityListAnchorLinksInlineRenderer : LinkInlineRenderer +{ + /********* + ** Public methods + *********/ + /// + protected override void Write(HtmlRenderer renderer, LinkInline link) + { + if (link.Url == "#") + { + string linkText = this.GetLinkText(link); + link.Url = '#' + PathUtilities.CreateSlug(linkText); + } + + base.Write(renderer, link); + } + + + /********* + ** Private methods + *********/ + /// Get the plaintext version of a link's display text. + /// The link whose text to get. + private string GetLinkText(LinkInline link) + { + // common case: single literal + if (link.FirstChild is LiteralInline inlineText && object.ReferenceEquals(link.FirstChild, link.LastChild)) + return inlineText.Content.ToString(); + + // else build it dynamically + StringBuilder builder = new(); + foreach (var inline in link) + { + if (inline is LiteralInline literal) + builder.Append(literal.Content.ToString()); + } + return builder.ToString(); + } +} diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 6d6cf6e57..949d4f262 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -63,10 +63,10 @@ public IEnumerable GetGameFolders() return new GameScanner().ScanIncludingInvalid(); } - /// Extract mod metadata from the wiki compatibility list. - public async Task GetWikiCompatibilityListAsync() + /// Extract mod metadata from the compatibility list repo. + public async Task GetCompatibilityListAsync() { - using WikiClient client = new(this.UserAgent); + using CompatibilityRepoClient client = new(this.UserAgent); return await client.FetchModsAsync(); } diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index dd3bab12e..34dd3aaf6 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -12,6 +12,7 @@ + diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 1827d9a95..e3f6c0cb1 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -38,8 +38,8 @@ internal class BackgroundService : IHostedService, IDisposable /// The background task server. private static BackgroundJobServer? JobServer; - /// The cache in which to store wiki metadata. - private static IWikiCacheRepository? WikiCache; + /// The cache in which to store compatibility list data. + private static ICompatibilityCacheRepository? CompatibilityCache; /// The cache in which to store mod data. private static IModCacheRepository? ModCache; @@ -69,14 +69,14 @@ internal class BackgroundService : IHostedService, IDisposable [MemberNotNullWhen(true, nameof(BackgroundService.JobServer), nameof(BackgroundService.ModCache), + nameof(BackgroundService.CompatibilityCache), nameof(BackgroundService.CurseForgeExportApiClient), nameof(BackgroundService.CurseForgeExportCache), nameof(BackgroundService.ModDropExportApiClient), nameof(BackgroundService.ModDropExportCache), nameof(BackgroundService.NexusExportApiClient), nameof(BackgroundService.NexusExportCache), - nameof(BackgroundService.UpdateCheckConfig), - nameof(BackgroundService.WikiCache) + nameof(BackgroundService.UpdateCheckConfig) )] private static bool IsStarted { get; set; } @@ -91,7 +91,7 @@ internal class BackgroundService : IHostedService, IDisposable ** Hosted service ****/ /// Construct an instance. - /// The cache in which to store wiki metadata. + /// The cache in which to store compatibility list data. /// The cache in which to store mod data. /// The cache in which to store mod data from the CurseForge export API. /// The HTTP client for fetching the mod export from the CurseForge export API. @@ -103,7 +103,7 @@ internal class BackgroundService : IHostedService, IDisposable /// The config settings for mod update checks. [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The Hangfire reference forces it to initialize first, since it's needed by the background service.")] public BackgroundService( - IWikiCacheRepository wikiCache, + ICompatibilityCacheRepository compatibilityCache, IModCacheRepository modCache, ICurseForgeExportCacheRepository curseForgeExportCache, ICurseForgeExportApiClient curseForgeExportApiClient, @@ -115,7 +115,7 @@ public BackgroundService( IOptions updateCheckConfig ) { - BackgroundService.WikiCache = wikiCache; + BackgroundService.CompatibilityCache = compatibilityCache; BackgroundService.ModCache = modCache; BackgroundService.CurseForgeExportApiClient = curseForgeExportApiClient; BackgroundService.CurseForgeExportCache = curseForgeExportCache; @@ -139,7 +139,7 @@ public Task StartAsync(CancellationToken cancellationToken) bool enableNexusExport = BackgroundService.NexusExportApiClient is not DisabledNexusExportApiClient; // set startup tasks - BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync(null)); + BackgroundJob.Enqueue(() => BackgroundService.UpdateCompatibilityListAsync(null)); if (enableCurseForgeExport) BackgroundJob.Enqueue(() => BackgroundService.UpdateCurseForgeExportAsync(null)); if (enableModDropExport) @@ -149,7 +149,7 @@ public Task StartAsync(CancellationToken cancellationToken) BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleModsAsync()); // set recurring tasks - RecurringJob.AddOrUpdate("update wiki data", () => BackgroundService.UpdateWikiAsync(null), "*/10 * * * *"); // every 10 minutes + RecurringJob.AddOrUpdate("update compatibility list", () => BackgroundService.UpdateCompatibilityListAsync(null), "*/10 * * * *"); // every 10 minutes if (enableCurseForgeExport) RecurringJob.AddOrUpdate("update CurseForge export", () => BackgroundService.UpdateCurseForgeExportAsync(null), "*/10 * * * *"); if (enableModDropExport) @@ -184,19 +184,19 @@ public void Dispose() /**** ** Tasks ****/ - /// Update the cached wiki metadata. + /// Update the cached compatibility list data. /// Information about the context in which the job is performed. This is injected automatically by Hangfire. [AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 60, 120])] - public static async Task UpdateWikiAsync(PerformContext? context) + public static async Task UpdateCompatibilityListAsync(PerformContext? context) { if (!BackgroundService.IsStarted) throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks."); - context.WriteLine("Fetching data from wiki..."); - WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); + context.WriteLine("Fetching data from compatibility repo..."); + ModCompatibilityEntry[] compatList = await new ModToolkit().GetCompatibilityListAsync(); context.WriteLine("Saving data..."); - BackgroundService.WikiCache.SaveWikiData(wikiCompatList.Mods); + BackgroundService.CompatibilityCache.SaveData(compatList); context.WriteLine("Done!"); } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 69c13c516..81dbc5dce 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -39,8 +39,8 @@ internal class ModsApiController : Controller /// The mod sites which provide mod metadata. private readonly ModSiteManager ModSites; - /// The cache in which to store wiki data. - private readonly IWikiCacheRepository WikiCache; + /// The cache in which to store compatibility list data. + private readonly ICompatibilityCacheRepository CompatibilityCache; /// The cache in which to store mod data. private readonly IModCacheRepository ModCache; @@ -57,7 +57,7 @@ internal class ModsApiController : Controller *********/ /// Construct an instance. /// The web hosting environment. - /// The cache in which to store wiki data. + /// The cache in which to store compatibility list data. /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. @@ -66,11 +66,11 @@ internal class ModsApiController : Controller /// The ModDrop API client. /// The Nexus API client. /// The API client for arbitrary update manifest URLs. - public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) + public ModsApiController(IWebHostEnvironment environment, ICompatibilityCacheRepository compatibilityCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); - this.WikiCache = wikiCache; + this.CompatibilityCache = compatibilityCache; this.ModCache = modCache; this.Config = config; this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus, updateManifest }); @@ -90,8 +90,8 @@ public async Task> PostAsync([FromBody] ModSearchMode ModUpdateCheckConfig config = this.Config.Value; - // fetch wiki data - WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray(); + // fetch compatibility list + ModCompatibilityEntry[] compatibilityList = this.CompatibilityCache.GetMods().Select(p => p.Data).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in model.Mods) { @@ -103,7 +103,7 @@ public async Task> PostAsync([FromBody] ModSearchMode mod.AddUpdateKeys(config.SmapiInfo.AddBetaUpdateKeys); // fetch result - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion, metrics); + ModEntryModel result = await this.GetModData(mod, compatibilityList, model.IncludeExtendedMetadata, model.ApiVersion, metrics); if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) { result.Errors = result.Errors @@ -131,17 +131,17 @@ public MetricsSummary GetMetrics() *********/ /// Get the metadata for a mod. /// The mod data to match. - /// The wiki data. + /// The compatibility list. /// Whether to include extended metadata for each mod. /// The SMAPI version installed by the player. /// The metrics to update with update-check results. /// Returns the mod data if found, else null. - private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion? apiVersion, ApiMetricsModel metrics) + private async Task GetModData(ModSearchEntryModel search, ModCompatibilityEntry[] compatibilityList, bool includeExtendedMetadata, ISemanticVersion? apiVersion, ApiMetricsModel metrics) { // cross-reference data ModDataRecord? record = this.ModDatabase.Get(search.ID); - WikiModEntry? wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase)); - UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); + ModCompatibilityEntry? compatEntry = compatibilityList.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase)); + UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, compatEntry).ToArray(); ModOverrideConfig? overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID.Trim(), StringComparison.OrdinalIgnoreCase)); bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false; @@ -165,7 +165,7 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod } // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.Overrides?.ChangeRemoteVersions, metrics); + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, compatEntry?.Overrides?.ChangeRemoteVersions, metrics); if (data.Status != RemoteModStatus.Ok) { errors.Add(data.Error ?? data.Status.ToString()); @@ -193,8 +193,8 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod } // get unofficial version - if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) - unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}"); + if (compatEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(compatEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(compatEntry.Compatibility.UnofficialVersion, optional?.Version)) + unofficial = new ModEntryVersionModel(compatEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{compatEntry.Anchor}"); // fallback to preview if latest is invalid if (main == null && optional != null) @@ -213,7 +213,7 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod } // get recommended update (if any) - ISemanticVersion? installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.Overrides?.ChangeLocalVersions, allowNonStandard: allowNonStandardVersions); + ISemanticVersion? installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), compatEntry?.Overrides?.ChangeLocalVersions, allowNonStandard: allowNonStandardVersions); if (apiVersion != null && installedVersion != null) { // get newer versions @@ -241,7 +241,7 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod // add extended metadata if (includeExtendedMetadata) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial); + result.Metadata = new ModExtendedMetadataModel(compatEntry, record, main: main, optional: optional, unofficial: unofficial); // add result result.Errors = errors.ToArray(); @@ -301,8 +301,8 @@ private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, b /// Get update keys based on the available mod metadata, while maintaining the precedence order. /// The specified update keys. /// The mod's entry in SMAPI's internal database. - /// The mod's entry in the wiki list. - private IEnumerable GetUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, WikiModEntry? entry) + /// The mod's entry in the compatibility list. + private IEnumerable GetUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, ModCompatibilityEntry? entry) { // get unique update keys List updateKeys = this.GetUnfilteredUpdateKeys(specifiedKeys, record, entry) @@ -310,7 +310,7 @@ private IEnumerable GetUpdateKeys(string[]? specifiedKeys, ModDataRec .Distinct() .ToList(); - // apply overrides from wiki + // apply overrides from compatibility list if (entry?.Overrides?.ChangeUpdateKeys?.HasChanges == true) { List newKeys = updateKeys.Select(p => p.ToString()).ToList(); @@ -336,8 +336,8 @@ private IEnumerable GetUpdateKeys(string[]? specifiedKeys, ModDataRec /// Get every available update key based on the available mod metadata, including duplicates and keys which should be filtered. /// The specified update keys. /// The mod's entry in SMAPI's internal database. - /// The mod's entry in the wiki list. - private IEnumerable GetUnfilteredUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, WikiModEntry? entry) + /// The mod's entry in the compatibility list. + private IEnumerable GetUnfilteredUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, ModCompatibilityEntry? entry) { // specified update keys foreach (string key in specifiedKeys ?? Array.Empty()) @@ -353,7 +353,7 @@ private IEnumerable GetUnfilteredUpdateKeys(string[]? specifiedKeys, Mod yield return defaultKey; } - // wiki metadata + // compatibility list metadata if (entry != null) { if (entry.NexusID.HasValue) diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index cef6aceac..d1654970b 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -17,9 +17,9 @@ internal class ModsController : Controller ** Fields *********/ /// The cache in which to store mod metadata. - private readonly IWikiCacheRepository Cache; + private readonly ICompatibilityCacheRepository Cache; - /// The number of minutes before which wiki data should be considered old. + /// The number of minutes before which compatibility list data should be considered old. private readonly int StaleMinutes; @@ -29,7 +29,7 @@ internal class ModsController : Controller /// Construct an instance. /// The cache in which to store mod metadata. /// The config settings for mod update checks. - public ModsController(IWikiCacheRepository cache, IOptions configProvider) + public ModsController(ICompatibilityCacheRepository cache, IOptions configProvider) { ModCompatibilityListConfig config = configProvider.Value; @@ -49,17 +49,17 @@ public ViewResult Index() /********* ** Private methods *********/ - /// Asynchronously fetch mod metadata from the wiki. + /// Asynchronously fetch mod metadata from the compatibility list. public ModListModel FetchData() { // fetch cached data - if (!this.Cache.TryGetWikiMetadata(out Cached? metadata)) + if (!this.Cache.TryGetCacheMetadata(out Cached? metadata)) return new ModListModel(Array.Empty(), lastUpdated: DateTimeOffset.UtcNow, isStale: true); // build model return new ModListModel( mods: this.Cache - .GetWikiMods() + .GetMods() .Select(mod => new ModModel(mod.Data)) .OrderBy(p => Regex.Replace((p.Name ?? "").ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting lastUpdated: metadata.LastUpdated, diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index a59e31152..50a0f9698 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -5,21 +5,21 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki; -/// Manages cached wiki data. -internal interface IWikiCacheRepository : ICacheRepository +/// Manages cached compatibility list data. +internal interface ICompatibilityCacheRepository : ICacheRepository { /********* ** Methods *********/ - /// Get the cached wiki metadata. + /// Get the cache metadata. /// The fetched metadata. - bool TryGetWikiMetadata([NotNullWhen(true)] out Cached? metadata); + bool TryGetCacheMetadata([NotNullWhen(true)] out Cached? metadata); - /// Get the cached wiki mods. + /// Get the cached compatibility list. /// A filter to apply, if any. - IEnumerable> GetWikiMods(Func? filter = null); + IEnumerable> GetMods(Func? filter = null); - /// Save data fetched from the wiki compatibility list. + /// Save data fetched from the compatibility list. /// The mod data. - void SaveWikiData(IEnumerable mods); + void SaveData(IEnumerable mods); } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs index 0ff8c4451..2b1bf3c53 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -6,33 +6,31 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki; -/// Manages cached wiki data in-memory. -internal class WikiCacheMemoryRepository : BaseCacheRepository, IWikiCacheRepository +/// Manages cached compatibility list data in-memory. +internal class CompatibilityCacheMemoryRepository : BaseCacheRepository, ICompatibilityCacheRepository { /********* ** Fields *********/ - /// The saved wiki metadata. - private Cached? Metadata; + /// The saved compatibility list metadata. + private Cached? Metadata; - /// The cached wiki data. - private Cached[] Mods = Array.Empty>(); + /// The cached compatibility list. + private Cached[] Mods = []; /********* ** Public methods *********/ - /// Get the cached wiki metadata. - /// The fetched metadata. - public bool TryGetWikiMetadata([NotNullWhen(true)] out Cached? metadata) + /// + public bool TryGetCacheMetadata([NotNullWhen(true)] out Cached? metadata) { metadata = this.Metadata; return metadata != null; } - /// Get the cached wiki mods. - /// A filter to apply, if any. - public IEnumerable> GetWikiMods(Func? filter = null) + /// + public IEnumerable> GetMods(Func? filter = null) { foreach (var mod in this.Mods) { @@ -41,11 +39,10 @@ public IEnumerable> GetWikiMods(Func? f } } - /// Save data fetched from the wiki compatibility list. - /// The mod data. - public void SaveWikiData(IEnumerable mods) + /// + public void SaveData(IEnumerable mods) { - this.Metadata = new Cached(new WikiMetadata()); - this.Mods = mods.Select(mod => new Cached(mod)).ToArray(); + this.Metadata = new Cached(new CompatibilityListMetadata()); + this.Mods = mods.Select(mod => new Cached(mod)).ToArray(); } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs index 324bd56f5..857149b1a 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs @@ -1,4 +1,4 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki; -/// The model for cached wiki metadata. -internal class WikiMetadata; +/// The model for cached compatibility list metadata. +internal class CompatibilityListMetadata; diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs index e1fb8bcd9..42ea5877d 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs @@ -6,6 +6,6 @@ internal class ModCompatibilityListConfig /********* ** Accessors *********/ - /// The number of minutes before which wiki data should be considered old. + /// The number of minutes before which compatibility list data should be considered old. public int StaleMinutes { get; set; } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 9c0bcc0ee..f175163b5 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -86,10 +86,10 @@ public void ConfigureServices(IServiceCollection services) // init storage services.AddSingleton(new ModCacheMemoryRepository()); + services.AddSingleton(new CompatibilityCacheMemoryRepository()); services.AddSingleton(new CurseForgeExportCacheMemoryRepository()); services.AddSingleton(new ModDropExportCacheMemoryRepository()); services.AddSingleton(new NexusExportCacheMemoryRepository()); - services.AddSingleton(new WikiCacheMemoryRepository()); // init Hangfire services diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs index 4e1e8bda0..3c653bb1d 100644 --- a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs +++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs @@ -41,12 +41,12 @@ public ModCompatibilityModel(string status, string? summary, string? brokeIn, Mo /// Construct an instance. /// The mod metadata. - public ModCompatibilityModel(WikiCompatibilityInfo info) + public ModCompatibilityModel(ModCompatibilityInfo info) { this.Status = info.Status.ToString(); this.Status = this.Status.Substring(0, 1).ToLower() + this.Status.Substring(1); - this.Summary = info.Summary; + this.Summary = info.HtmlSummary ?? info.Summary; this.BrokeIn = info.BrokeIn; if (info.UnofficialVersion != null) this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl!, info.UnofficialVersion.ToString()); diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 008dbeddb..1eec86cc0 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -82,7 +82,7 @@ public ModModel(string? name, string alternateNames, string author, string alter /// Construct an instance. /// The mod metadata. - public ModModel(WikiModEntry entry) + public ModModel(ModCompatibilityEntry entry) { // basic info this.Name = entry.Name.FirstOrDefault(); @@ -104,7 +104,7 @@ public ModModel(WikiModEntry entry) *********/ /// Get the web URL for the mod's source code repository, if any. /// The mod metadata. - private string? GetSourceUrl(WikiModEntry entry) + private string? GetSourceUrl(ModCompatibilityEntry entry) { if (!string.IsNullOrWhiteSpace(entry.GitHubRepo)) return $"https://github.com/{entry.GitHubRepo}"; @@ -115,7 +115,7 @@ public ModModel(WikiModEntry entry) /// Get the web URLs for the mod pages, if any. /// The mod metadata. - private IEnumerable GetModPageUrls(WikiModEntry entry) + private IEnumerable GetModPageUrls(ModCompatibilityEntry entry) { foreach ((ModSiteKey modSite, string url) in entry.GetModPageUrls()) { diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 91c22d713..0dc28c198 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -29,14 +29,14 @@ else { @if (Model.IsStale) { -
Showing data from @staleAge.Humanize(maxUnit: TimeUnit.Hour, minUnit: TimeUnit.Minute) ago. (Couldn't fetch newer data; the wiki API may be offline.)
+
Showing data from @staleAge.Humanize(maxUnit: TimeUnit.Hour, minUnit: TimeUnit.Minute) ago. (Couldn't fetch newer data; the GitHub API may be offline.)
}
-

This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check the troubleshooting guide or ask for help.

+

This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check the troubleshooting guide or ask for help.

-

The list is updated every few days (you can help update it!). It doesn't include XNB mods (see using XNB mods on the wiki instead) or compatible content packs.

+

The list is updated regularly (you can help update it!). It doesn't include XNB mods (see using XNB mods on the wiki instead) or compatible content packs.

From 421a6255d8d0df17426a9a0e7351ba5524132032 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2024 01:13:37 -0500 Subject: [PATCH 05/16] rename files for compatibility list revamp --- src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs | 2 +- .../Clients/{Wiki => CompatibilityRepo}/ChangeDescriptor.cs | 2 +- .../CompatibilityRepoClient.cs} | 6 +++--- .../Internal/DataModels/RawCompatibilityList.cs | 2 +- .../Internal/DataModels/RawModDataOverride.cs | 2 +- .../Internal/DataModels/RawModEntry.cs | 2 +- .../Internal/DataModels/RawModUnofficialUpdate.cs | 2 +- .../Internal/SetResponseTypeFilter.cs | 2 +- .../ModCompatibilityEntry.cs} | 2 +- .../ModCompatibilityInfo.cs} | 2 +- .../ModCompatibilityStatus.cs} | 2 +- .../ModDataOverrideEntry.cs} | 2 +- .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 2 +- src/SMAPI.Toolkit/ModToolkit.cs | 2 +- src/SMAPI.Web/BackgroundService.cs | 4 ++-- src/SMAPI.Web/Controllers/ModsApiController.cs | 4 ++-- src/SMAPI.Web/Controllers/ModsController.cs | 2 +- .../CompatibilityCacheMemoryRepository.cs} | 4 ++-- .../CompatibilityListMetadata.cs} | 2 +- .../ICompatibilityCacheRepository.cs} | 4 ++-- src/SMAPI.Web/Framework/ModSiteManager.cs | 2 +- src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs | 2 +- src/SMAPI.Web/ViewModels/ModModel.cs | 2 +- 24 files changed, 30 insertions(+), 30 deletions(-) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki => CompatibilityRepo}/ChangeDescriptor.cs (99%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki/WikiClient.cs => CompatibilityRepo/CompatibilityRepoClient.cs} (97%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki => CompatibilityRepo}/Internal/DataModels/RawCompatibilityList.cs (84%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki => CompatibilityRepo}/Internal/DataModels/RawModDataOverride.cs (86%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki => CompatibilityRepo}/Internal/DataModels/RawModEntry.cs (97%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki => CompatibilityRepo}/Internal/DataModels/RawModUnofficialUpdate.cs (81%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki => CompatibilityRepo}/Internal/SetResponseTypeFilter.cs (87%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki/WikiModEntry.cs => CompatibilityRepo/ModCompatibilityEntry.cs} (98%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki/WikiCompatibilityInfo.cs => CompatibilityRepo/ModCompatibilityInfo.cs} (96%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki/WikiCompatibilityStatus.cs => CompatibilityRepo/ModCompatibilityStatus.cs} (92%) rename src/SMAPI.Toolkit/Framework/Clients/{Wiki/WikiDataOverrideEntry.cs => CompatibilityRepo/ModDataOverrideEntry.cs} (93%) rename src/SMAPI.Web/Framework/Caching/{Wiki/WikiCacheMemoryRepository.cs => CompatibilityRepo/CompatibilityCacheMemoryRepository.cs} (91%) rename src/SMAPI.Web/Framework/Caching/{Wiki/WikiMetadata.cs => CompatibilityRepo/CompatibilityListMetadata.cs} (62%) rename src/SMAPI.Web/Framework/Caching/{Wiki/IWikiCacheRepository.cs => CompatibilityRepo/ICompatibilityCacheRepository.cs} (86%) diff --git a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs index fb6c0ea15..8870cdcd1 100644 --- a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs +++ b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs @@ -2,7 +2,7 @@ using FluentAssertions; using NUnit.Framework; using StardewModdingAPI; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; namespace SMAPI.Tests.WikiClient; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs similarity index 99% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs index b1efc6b7a..6cdf7dc19 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ChangeDescriptor.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; /// A set of changes which can be applied to a mod data field. public class ChangeDescriptor diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs similarity index 97% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index 2c2ede2fa..0b3286232 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -3,12 +3,12 @@ using System.Threading.Tasks; using Markdig; using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; using StardewModdingAPI.Toolkit.Framework.MarkdownExtensions; using StardewModdingAPI.Toolkit.Utilities; -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; /// An HTTP client for fetching data from the mod compatibility repo. public class CompatibilityRepoClient : IDisposable diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawCompatibilityList.cs similarity index 84% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawCompatibilityList.cs index 4dd5826fd..ec457d813 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawCompatibilityList.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawCompatibilityList.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; /// The main data model for the raw compatibility data. internal class RawCompatibilityList diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModDataOverride.cs similarity index 86% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModDataOverride.cs index bee41d284..2f129c9f6 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModDataOverride.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModDataOverride.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; /// As part of , a data override to apply to the mod's manifest or remote mod page data. internal class RawModDataOverride diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs similarity index 97% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs index d6b85b925..dea30eba5 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; /// The compatibility metadata for a mod in the raw data. internal class RawModEntry diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModUnofficialUpdate.cs similarity index 81% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModUnofficialUpdate.cs index 7e2b64e2a..c5115cf88 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/DataModels/RawModUnofficialUpdate.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModUnofficialUpdate.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; /// As part of , an unofficial update which fixes compatibility with the latest Stardew Valley and SMAPI versions. internal class RawModUnofficialUpdate diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs similarity index 87% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs index 2c8119223..17739d2d0 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/Internal/SetResponseTypeFilter.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs @@ -2,7 +2,7 @@ using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki.Internal; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal; /// An HTTP filter which sets the content type for all responses received to text/json. internal class ForceJsonResponseTypeFilter : IHttpFilter diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs similarity index 98% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs index 460824540..a53acaa8b 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using StardewModdingAPI.Toolkit.Framework.UpdateData; -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; /// A mod entry in the compatibility list. public class ModCompatibilityEntry diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs similarity index 96% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs index f8ad2f691..f099b9919 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; /// Compatibility info for a mod. public class ModCompatibilityInfo diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityStatus.cs similarity index 92% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityStatus.cs index 96b8c4a30..1b859f554 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityStatus.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; /// The compatibility status for a mod. public enum ModCompatibilityStatus diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModDataOverrideEntry.cs similarity index 93% rename from src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModDataOverrideEntry.cs index 1e35005bc..04e1f9fd1 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModDataOverrideEntry.cs @@ -1,6 +1,6 @@ using System; -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; /// The data overrides to apply to matching mods. public class ModDataOverrideEntry diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 565c20066..9a9512917 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -3,7 +3,7 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; using StardewModdingAPI.Toolkit.Framework.ModData; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi; diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 949d4f262..2032843e2 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; using StardewModdingAPI.Toolkit.Framework.GameScanning; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index e3f6c0cb1..4849bbb20 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -11,16 +11,16 @@ using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; using StardewModdingAPI.Toolkit.Framework.Clients.CurseForgeExport; using StardewModdingAPI.Toolkit.Framework.Clients.ModDropExport; using StardewModdingAPI.Toolkit.Framework.Clients.NexusExport; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Web.Framework.Caching; +using StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; using StardewModdingAPI.Web.Framework.Caching.CurseForgeExport; using StardewModdingAPI.Web.Framework.Caching.ModDropExport; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.NexusExport; -using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 81dbc5dce..9e440a2ba 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -8,14 +8,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Caching; +using StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; using StardewModdingAPI.Web.Framework.Caching.Mods; -using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.CurseForge; diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index d1654970b..f15c2c2e4 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework.Caching; -using StardewModdingAPI.Web.Framework.Caching.Wiki; +using StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/CompatibilityRepo/CompatibilityCacheMemoryRepository.cs similarity index 91% rename from src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs rename to src/SMAPI.Web/Framework/Caching/CompatibilityRepo/CompatibilityCacheMemoryRepository.cs index 2b1bf3c53..3403bc35e 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/CompatibilityRepo/CompatibilityCacheMemoryRepository.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; -namespace StardewModdingAPI.Web.Framework.Caching.Wiki; +namespace StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; /// Manages cached compatibility list data in-memory. internal class CompatibilityCacheMemoryRepository : BaseCacheRepository, ICompatibilityCacheRepository diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/CompatibilityRepo/CompatibilityListMetadata.cs similarity index 62% rename from src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs rename to src/SMAPI.Web/Framework/Caching/CompatibilityRepo/CompatibilityListMetadata.cs index 857149b1a..a5ed2344c 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/CompatibilityRepo/CompatibilityListMetadata.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Web.Framework.Caching.Wiki; +namespace StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; /// The model for cached compatibility list metadata. internal class CompatibilityListMetadata; diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/CompatibilityRepo/ICompatibilityCacheRepository.cs similarity index 86% rename from src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs rename to src/SMAPI.Web/Framework/Caching/CompatibilityRepo/ICompatibilityCacheRepository.cs index 50a0f9698..8f9141d2b 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/CompatibilityRepo/ICompatibilityCacheRepository.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; -namespace StardewModdingAPI.Web.Framework.Caching.Wiki; +namespace StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; /// Manages cached compatibility list data. internal interface ICompatibilityCacheRepository : ICacheRepository diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 46d98b0d7..33a165c65 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using StardewModdingAPI.Toolkit; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients; diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index f175163b5..f9ccb55ae 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -17,11 +17,11 @@ using StardewModdingAPI.Toolkit.Framework.Clients.NexusExport; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Caching.CompatibilityRepo; using StardewModdingAPI.Web.Framework.Caching.CurseForgeExport; using StardewModdingAPI.Web.Framework.Caching.ModDropExport; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.NexusExport; -using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs index 3c653bb1d..27a7eb8ba 100644 --- a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs +++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; namespace StardewModdingAPI.Web.ViewModels; diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 1eec86cc0..8f9948167 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.ViewModels; From d1871706fb0592dfc4fab1ec11a499cb002c90b9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Nov 2024 21:06:55 -0500 Subject: [PATCH 06/16] fix anchor handling after compatibility list revamp --- .../Utilities/PathUtilitiesTests.cs | 2 ++ .../CompatibilityRepoClient.cs | 2 +- ...patibilityListAnchorLinksInlineRenderer.cs | 2 +- src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 23 ++++++++++++------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index d8b77835e..69cf348ec 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -291,6 +291,7 @@ public string GetRelativePath(string sourceDir, string targetPath) [TestCase(" example", ExpectedResult = false)] [TestCase("example ", ExpectedResult = false)] [TestCase("exa mple", ExpectedResult = false)] + [TestCase("exa - mple", ExpectedResult = false)] [TestCase("exam!ple", ExpectedResult = false)] [TestCase("example?", ExpectedResult = false)] [TestCase("#example", ExpectedResult = false)] @@ -319,6 +320,7 @@ public bool IsSlug(string input) [TestCase(" example", ExpectedResult = "example")] [TestCase("example ", ExpectedResult = "example-")] [TestCase("exa mple", ExpectedResult = "exa-mple")] + [TestCase("exa - mple", ExpectedResult = "exa-mple")] [TestCase("exam!ple", ExpectedResult = "exam-ple")] [TestCase("example?", ExpectedResult = "example-")] [TestCase("#example", ExpectedResult = "example")] diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index 0b3286232..41e138b3a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -171,7 +171,7 @@ private ModCompatibilityEntry ParseModEntry(RawModEntry rawModEntry) warnings: rawModEntry.Warnings ?? Array.Empty(), devNote: rawModEntry.DeveloperNotes, overrides: this.ParseOverrideEntries(modIds, rawModEntry.Overrides), - anchor: PathUtilities.CreateSlug(modIds.FirstOrDefault())?.ToLower() + anchor: PathUtilities.CreateSlug(modNames.FirstOrDefault())?.ToLower() ); } diff --git a/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs b/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs index 2c86fe6a0..e6c0e57f2 100644 --- a/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs +++ b/src/SMAPI.Toolkit/Framework/MarkdownExtensions/ExpandCompatibilityListAnchorLinksInlineRenderer.cs @@ -18,7 +18,7 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) if (link.Url == "#") { string linkText = this.GetLinkText(link); - link.Url = '#' + PathUtilities.CreateSlug(linkText); + link.Url = '#' + PathUtilities.CreateSlug(linkText).ToLower(); } base.Write(renderer, link); diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 4626bdc4a..4a1e6930f 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -16,9 +16,6 @@ public static class PathUtilities /// The root prefix for a Windows UNC path. private const string WindowsUncRoot = @"\\"; - /// The regex characters that are allowed in a 'slug' that can be used in many different contexts, formatted for use in a [] regex character class. - private const string SlugCharacterClass = @"\p{L}\d\-_\."; // Unicode 'letter' character, digit, dash, underscore, or period - /********* ** Accessors @@ -179,26 +176,36 @@ public static bool IsSafeRelativePath(string? path) && PathUtilities.GetSegments(path).All(segment => segment.Trim() != ".."); } - /// Create a 'slug' containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// Create a 'slug' containing only basic characters that are safe in all contexts like filenames, URLs, etc. /// The string to represent. + /// The behavior of this method isn't guaranteed to remain unchanged. You should only use this method is cases where you can use it consistently and the values aren't stored across different versions of SMAPI. [Pure] #if NET6_0_OR_GREATER [return: NotNullIfNotNull("input")] #endif public static string? CreateSlug(string? input) { + // + // This pattern is synced with IsSlug below. + // + return string.IsNullOrWhiteSpace(input) ? input - : Regex.Replace(input, "[^" + PathUtilities.SlugCharacterClass + "]+", "-").TrimStart('-'); + : Regex.Replace(input, @"[^\p{L}\d_\.]+", "-").TrimStart('-'); } - /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts like filenames, URLs, etc. /// The string to check. + /// The behavior of this method isn't guaranteed to remain unchanged. You should only use this method is cases where you can use it consistently and the values aren't stored across different versions of SMAPI. [Pure] public static bool IsSlug(string? str) { + // + // This uses the same pattern as CreateSlug, with the addition of '-'. + // + return - string.IsNullOrWhiteSpace(str) - || !Regex.IsMatch(str, "[^+" + PathUtilities.SlugCharacterClass + "]", RegexOptions.IgnoreCase); + string.IsNullOrEmpty(str) + || !Regex.IsMatch(str, @"[^\p{L}\d_\.\-]", RegexOptions.IgnoreCase); } } From c5ae521c8f65711e60842b8315d8d3f6b1b4dfbf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Nov 2024 21:06:55 -0500 Subject: [PATCH 07/16] fix data overrides after compatibility list revamp --- .../Clients/CompatibilityRepo/CompatibilityRepoClient.cs | 2 +- .../CompatibilityRepo/Internal/DataModels/RawModEntry.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index 41e138b3a..72e55bd80 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -170,7 +170,7 @@ private ModCompatibilityEntry ParseModEntry(RawModEntry rawModEntry) ), warnings: rawModEntry.Warnings ?? Array.Empty(), devNote: rawModEntry.DeveloperNotes, - overrides: this.ParseOverrideEntries(modIds, rawModEntry.Overrides), + overrides: this.ParseOverrideEntries(modIds, rawModEntry.OverrideModData), anchor: PathUtilities.CreateSlug(modNames.FirstOrDefault())?.ToLower() ); } diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs index dea30eba5..114523bf4 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs @@ -73,5 +73,5 @@ internal class RawModEntry ** Data overrides ****/ /// The data overrides to apply to the mod's manifest or remote mod page data, if any. - public RawModDataOverride[]? Overrides { get; set; } + public RawModDataOverride[]? OverrideModData { get; set; } } From a0e2c0ec42a66bae1443cdf2f2eae603bfafa756 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Nov 2024 21:06:55 -0500 Subject: [PATCH 08/16] simplify working with compatibility list data through SMAPI toolkit --- build/deploy-local-smapi.targets | 1 + build/unix/prepare-install-package.sh | 2 +- build/windows/prepare-install-package.ps1 | 2 +- docs/release-notes.md | 3 + .../CompatibilityRepoClient.cs | 117 ++++-------------- .../Internal/SetResponseTypeFilter.cs | 2 +- .../RawCompatibilityList.cs | 4 +- .../RawModDataOverride.cs | 4 +- .../RawModEntry.cs | 106 +++++++++++++++- .../RawModUnofficialUpdate.cs | 4 +- src/SMAPI.Toolkit/ModToolkit.cs | 14 ++- 11 files changed, 156 insertions(+), 103 deletions(-) rename src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/{Internal/DataModels => RawDataModels}/RawCompatibilityList.cs (89%) rename src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/{Internal/DataModels => RawDataModels}/RawModDataOverride.cs (90%) rename src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/{Internal/DataModels => RawDataModels}/RawModEntry.cs (51%) rename src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/{Internal/DataModels => RawDataModels}/RawModUnofficialUpdate.cs (86%) diff --git a/build/deploy-local-smapi.targets b/build/deploy-local-smapi.targets index bd84ee11b..c9887a909 100644 --- a/build/deploy-local-smapi.targets +++ b/build/deploy-local-smapi.targets @@ -21,6 +21,7 @@ This assumes `find-game-folder.targets` has already been imported and validated. + diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index c03bb09a2..fb723b31e 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -134,7 +134,7 @@ for folder in ${folders[@]}; do cp -r "$smapiBin/i18n" "$bundlePath/smapi-internal" # bundle smapi-internal - for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pathoschild.Http.Client.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.xml" "System.Net.Http.Formatting.dll"; do + for name in "0Harmony.dll" "0Harmony.xml" "Markdig.dll" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pathoschild.Http.Client.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.xml" "System.Net.Http.Formatting.dll"; do cp "$smapiBin/$name" "$bundlePath/smapi-internal" done diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index e6d7cd474..25667118c 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -155,7 +155,7 @@ foreach ($folder in $folders) { cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal" # bundle smapi-internal - foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) { + foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Markdig.dll", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) { cp "$smapiBin/$name" "$bundlePath/smapi-internal" } diff --git a/docs/release-notes.md b/docs/release-notes.md index f182c6f71..4000a601d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,9 @@ * Revamped how the mod compatibility list works to simplify maintenance. * Fixed CurseForge links not shown for mods that have a CurseForge page. +* For external tools: + * Added method to the SMAPI toolkit to get the URL for a given update site key + mod ID. + ## 4.1.7 Released 12 November 2024 for Stardew Valley 1.6.14 or later. diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index 72e55bd80..2d841b9cf 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -4,7 +4,7 @@ using Markdig; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal; -using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; +using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.RawDataModels; using StardewModdingAPI.Toolkit.Framework.MarkdownExtensions; using StardewModdingAPI.Toolkit.Utilities; @@ -48,101 +48,43 @@ public async Task FetchModsAsync() return (response.Mods ?? Array.Empty()) .Concat(response.BrokenContentPacks ?? Array.Empty()) - .Select(this.ParseModEntry) + .Select(this.ParseRawModEntry) .ToArray(); } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + /// Get the inline HTML produced by a Markdown string in a compatibility repo field. + /// The Markdown to parse. + /// This is a low-level method. Most code should use instead. + public string ParseMarkdownToInlineHtml(string markdown) { - this.Client.Dispose(); + string html = Markdown.ToHtml(markdown, this.MarkdownPipeline); + + // Markdown wraps all content with

, and there's no non-hacky way to disable it. + // We need to strip them since the content is shown inline. + html = html.Trim(); + if (html.StartsWith("

", StringComparison.OrdinalIgnoreCase) && html.EndsWith("

", StringComparison.OrdinalIgnoreCase) && html.IndexOf("

", 3, StringComparison.OrdinalIgnoreCase) == -1) + html = html.Substring(3, html.Length - 7); + + return html; } - /********* - ** Private methods - *********/ ///

Parse a mod compatibility entry. /// The HTML compatibility entries. - private ModCompatibilityEntry ParseModEntry(RawModEntry rawModEntry) + /// This is a low-level method. Most code should use instead. + public ModCompatibilityEntry ParseRawModEntry(RawModEntry rawModEntry) { // parse main fields string[] modIds = this.GetCsv(rawModEntry.Id); string[] modNames = this.GetCsv(rawModEntry.Name); string[] authorNames = this.GetCsv(rawModEntry.Author); - - // parse status - if (!Enum.TryParse(rawModEntry.Status, true, out ModCompatibilityStatus status)) - { - if (rawModEntry.UnofficialUpdate != null) - status = ModCompatibilityStatus.Unofficial; - else if (rawModEntry.BrokeIn != null) - status = ModCompatibilityStatus.Broken; - else - status = ModCompatibilityStatus.Ok; - } - - // parse summary - bool hasSource = rawModEntry.GitHub != null || rawModEntry.Source != null; - char summaryIcon = status switch - { - ModCompatibilityStatus.Unofficial or ModCompatibilityStatus.Workaround => '⚠', - ModCompatibilityStatus.Broken when hasSource => '↻', - ModCompatibilityStatus.Broken or ModCompatibilityStatus.Obsolete or ModCompatibilityStatus.Abandoned => '✖', - _ => '✓' - }; - string? summary = rawModEntry.Summary; - bool hasMarkdown = summary != null; - if (summary is null) - { - switch (status) - { - case ModCompatibilityStatus.Ok: - summary = "use latest version."; - break; - - case ModCompatibilityStatus.Optional: - summary = "use optional download."; - break; - - case ModCompatibilityStatus.Unofficial: - summary = $"broken, use [unofficial version]({rawModEntry.UnofficialUpdate?.Url})"; - if (rawModEntry.UnofficialUpdate?.Version != null) - summary += $" ({rawModEntry.UnofficialUpdate.Version})"; - summary += '.'; - hasMarkdown = true; - break; - - case ModCompatibilityStatus.Workaround: - summary = "broken [**error:** should specify summary]."; - hasMarkdown = true; - break; - - case ModCompatibilityStatus.Broken: - summary = hasSource - ? "broken, not updated yet." - : "broken, not open-source."; - break; - - case ModCompatibilityStatus.Obsolete: - summary = "remove this mod (obsolete)."; - break; - - case ModCompatibilityStatus.Abandoned: - summary = "remove this mod (no longer maintained)."; - break; - - default: - summary = $"[**error:** unknown status '{status}'.]"; - break; - } - } - summary = $"{summaryIcon} {summary}"; + ModCompatibilityStatus status = rawModEntry.GetStatus(); + rawModEntry.GetCompatibilitySummary(out string summary, out bool hasMarkdown); // get HTML summary string? htmlSummary = null; if (hasMarkdown) { - htmlSummary = this.ToInlineHtml(summary); + htmlSummary = this.ParseMarkdownToInlineHtml(summary); if (htmlSummary == summary) htmlSummary = null; } @@ -175,21 +117,16 @@ private ModCompatibilityEntry ParseModEntry(RawModEntry rawModEntry) ); } - /// Get the inline HTML produced by a Markdown string. - /// The Markdown to parse. - private string ToInlineHtml(string markdown) + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() { - string html = Markdown.ToHtml(markdown, this.MarkdownPipeline); - - // Markdown wraps all content with

, and there's no non-hacky way to disable it. - // We need to strip them since the content is shown inline. - html = html.Trim(); - if (html.StartsWith("

", StringComparison.OrdinalIgnoreCase) && html.EndsWith("

", StringComparison.OrdinalIgnoreCase) && html.IndexOf("

", 3, StringComparison.OrdinalIgnoreCase) == -1) - html = html.Substring(3, html.Length - 7); - - return html; + this.Client.Dispose(); } + + /********* + ** Private methods + *********/ ///

Parse valid mod data override entries. /// The mod's unique IDs. /// The raw data override entries to parse. diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs index 17739d2d0..e0eb68f3d 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/SetResponseTypeFilter.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal; /// An HTTP filter which sets the content type for all responses received to text/json. -internal class ForceJsonResponseTypeFilter : IHttpFilter +public class ForceJsonResponseTypeFilter : IHttpFilter { /// public void OnRequest(IRequest request) { } diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawCompatibilityList.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawCompatibilityList.cs similarity index 89% rename from src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawCompatibilityList.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawCompatibilityList.cs index ec457d813..f36ed6d71 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawCompatibilityList.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawCompatibilityList.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.RawDataModels; /// The main data model for the raw compatibility data. -internal class RawCompatibilityList +public class RawCompatibilityList { /// The compatibility data for C# SMAPI mods. public RawModEntry[]? Mods { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModDataOverride.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModDataOverride.cs similarity index 90% rename from src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModDataOverride.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModDataOverride.cs index 2f129c9f6..85f678e86 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModDataOverride.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModDataOverride.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.RawDataModels; /// As part of , a data override to apply to the mod's manifest or remote mod page data. -internal class RawModDataOverride +public class RawModDataOverride { /// The data type to override. public string? Type { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs similarity index 51% rename from src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs index 114523bf4..ec8d19ab2 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs @@ -1,7 +1,9 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; +using System; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.RawDataModels; /// The compatibility metadata for a mod in the raw data. -internal class RawModEntry +public class RawModEntry { /********* ** Properties @@ -74,4 +76,104 @@ internal class RawModEntry ****/ /// The data overrides to apply to the mod's manifest or remote mod page data, if any. public RawModDataOverride[]? OverrideModData { get; set; } + + + /********* + ** Public methods + *********/ + /// Get whether this mod has a source code link. + public bool HasSource() + { + return + !string.IsNullOrWhiteSpace(this.GitHub) + || !string.IsNullOrWhiteSpace(this.Source); + } + + /// Get the mod's compatibility status based on its fields. + public ModCompatibilityStatus GetStatus() + { + if (!Enum.TryParse(this.Status, true, out ModCompatibilityStatus status)) + { + if (this.UnofficialUpdate != null) + status = ModCompatibilityStatus.Unofficial; + else if (this.BrokeIn != null) + status = ModCompatibilityStatus.Broken; + else + status = ModCompatibilityStatus.Ok; + } + + return status; + } + + /// Get the compatibility summary as shown on the compatibility list. + /// The compatibility summary. + /// Whether the summary may contain Markdown formatting. + public void GetCompatibilitySummary(out string summary, out bool mayBeMarkdown) + { + // get mod info + bool hasSource = this.HasSource(); + ModCompatibilityStatus status = this.GetStatus(); + + // get icon + char statusIcon = status switch + { + ModCompatibilityStatus.Unofficial or ModCompatibilityStatus.Workaround => '⚠', + ModCompatibilityStatus.Broken when hasSource => '↻', + ModCompatibilityStatus.Broken or ModCompatibilityStatus.Obsolete or ModCompatibilityStatus.Abandoned => '✖', + _ => '✓' + }; + + // get summary + if (this.Summary is not null) + { + summary = $"{statusIcon} {this.Summary}"; + mayBeMarkdown = true; + } + else + { + mayBeMarkdown = false; + + switch (status) + { + case ModCompatibilityStatus.Ok: + summary = "use latest version."; + break; + + case ModCompatibilityStatus.Optional: + summary = "use optional download."; + break; + + case ModCompatibilityStatus.Unofficial: + summary = $"broken, use [unofficial version]({this.UnofficialUpdate?.Url})"; + if (this.UnofficialUpdate?.Version != null) + summary += $" ({this.UnofficialUpdate.Version})"; + summary += '.'; + mayBeMarkdown = true; + break; + + case ModCompatibilityStatus.Workaround: + summary = "broken [**error:** should specify summary]."; + mayBeMarkdown = true; + break; + + case ModCompatibilityStatus.Broken: + summary = hasSource + ? "broken, not updated yet." + : "broken, not open-source."; + break; + + case ModCompatibilityStatus.Obsolete: + summary = "remove this mod (obsolete)."; + break; + + case ModCompatibilityStatus.Abandoned: + summary = "remove this mod (no longer maintained)."; + break; + + default: + summary = $"[**error:** unknown status '{status}'.]"; + break; + } + } + } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModUnofficialUpdate.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModUnofficialUpdate.cs similarity index 86% rename from src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModUnofficialUpdate.cs rename to src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModUnofficialUpdate.cs index c5115cf88..633feca66 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/Internal/DataModels/RawModUnofficialUpdate.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModUnofficialUpdate.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.Internal.DataModels; +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo.RawDataModels; /// As part of , an unofficial update which fixes compatibility with the latest Stardew Valley and SMAPI versions. -internal class RawModUnofficialUpdate +public class RawModUnofficialUpdate { /// The version of the unofficial update. public string? Version { get; set; } diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 2032843e2..b02849668 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -98,14 +98,24 @@ public IEnumerable GetModFolders(string rootPath, string modPath, boo /// Get an update URL for an update key (if valid). /// The update key. + /// Returns the URL if the mod site is supported, else null. public string? GetUpdateUrl(string updateKey) { UpdateKey parsed = UpdateKey.Parse(updateKey); if (!parsed.LooksValid) return null; - if (this.VendorModUrls.TryGetValue(parsed.Site, out string? urlTemplate)) - return string.Format(urlTemplate, parsed.ID); + return this.GetUpdateUrl(parsed.Site, parsed.ID); + } + + /// Get an update URL for an update key (if valid). + /// The mod site containing the mod. + /// The mod ID within the site. + /// Returns the URL if the mod site is supported, else null. + public string? GetUpdateUrl(ModSiteKey site, string id) + { + if (this.VendorModUrls.TryGetValue(site, out string? urlTemplate)) + return string.Format(urlTemplate, id); return null; } From 63f902b4169e80f1de5b4b1bbe7df63e41cddd96 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Nov 2024 23:13:58 -0500 Subject: [PATCH 09/16] update mod compatibility list --- docs/release-notes.md | 6 +- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 80 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4000a601d..1d4f8c87c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For players: + * Updated mod compatibility list. + * For mod authors: * Added `PathUtilities.CreateSlug` to turn an arbitrary string into a safe 'slug' that can be used in special contexts like URLs and file paths. _For example, `PathUtilities.CreateSlug("some 例子?!/\\~ text")` becomes `"some-例子-text"`._ @@ -12,7 +15,8 @@ * Fixed CurseForge links not shown for mods that have a CurseForge page. * For external tools: - * Added method to the SMAPI toolkit to get the URL for a given update site key + mod ID. + * Updated SMAPI toolkit for the new [mod compatibility repo](https://github.com/Pathoschild/SmapiCompatibilityList), which replaces the former [wiki page](https://stardewvalleywiki.com/Modding:Mod_compatibility). + * Added toolkit method to get the URL from an update key site + mod ID. ## 4.1.7 Released 12 November 2024 for Stardew Valley 1.6.14 or later. diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 764446293..021c3c8b4 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -143,11 +143,36 @@ /********* ** Broke in SDV 1.6.9 *********/ + "Artifact Digger": { + "ID": "mizzion.artifactdigger", + "~1.3.0 | Status": "AssumeBroken", + "~1.3.0 | StatusReasonDetails": "content loads fail at runtime" + }, + "Better Crystalariums": { + "ID": "DecidedlyHuman.BetterCrystalariums", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Better Frog Control": { + "ID": "malkavian.betterfrogcontrol", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "CompanionTrinketEffect type reference fails at runtime" + }, "Catalogue Indicator": { "ID": "com.anthonyhilyard.CatalogueIndicator", "~1.0.0 | Status": "AssumeBroken", "~1.0.0 | StatusReasonDetails": "ItemStockInformation references crash at runtime" }, + "Custom Moss": { + "ID": "aceynk.CustomMoss", + "~1.1.1 | Status": "AssumeBroken", + "~1.1.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Fishing Tackle Tooltip": { + "ID": "BarleyZP.FishingTackleTooltip", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "Happy Home Designer": { "ID": "tlitookilakin.HappyHomeDesigner", "~2.1.2 | Status": "AssumeBroken", @@ -158,6 +183,11 @@ "~1.0.6 | Status": "AssumeBroken", "~1.0.6 | StatusReasonDetails": "breaks question dialogues (e.g. Lewis' start-festival-event question)" }, + "Jump Down the Mine": { + "ID": "flowoeB.JumpDownTheMine", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "fails with runtime 'Common Language Runtime detected an invalid program' errors" + }, "Json Assets": { "ID": "spacechase0.JsonAssets", "~1.1.18 | Status": "AssumeBroken", @@ -165,26 +195,61 @@ "Default | UpdateKey": "Nexus:1720" }, + "Luck Skill": { + "ID": "spacechase0.LuckSkill", + "~1.2.6 | Status": "AssumeBroken", + "~1.2.6 | StatusReasonDetails": "Harmony errors at runtime" + }, "Lunar Disturbances": { "ID": "KoihimeNakamura.LunarDisturbances", "~1.5.3 | Status": "AssumeBroken", "~1.5.3 | StatusReasonDetails": "ItemStockInformation references crash at runtime" }, + "Machine Augmentors": { + "ID": "SlayerDharok.MachineAugmentors", + "~1.1.0 | Status": "AssumeBroken", + "~1.1.0 | StatusReasonDetails": "ItemStockInformation references crash at runtime" + }, + "NPC Map Locations Extra Config": { + "ID": "trashcan9.NPCMapLocations.ExtraConfig", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "PokefyMania": { + "ID": "NoodlesofCat.PokefyMania", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "content edits fail at runtime" + }, "Searchable Chests": { "ID": "ThisIsCad.SearchableChests", "~1.2.0 | Status": "AssumeBroken", "~1.2.0 | StatusReasonDetails": "ItemStockInformation references crash at runtime" }, + "Season Switcher": { + "ID": "Dumbledalton.SeasonSwitcher", + "~0.1.0 | Status": "AssumeBroken", + "~0.1.0 | StatusReasonDetails": "fails with runtime 'Common Language Runtime detected an invalid program' errors" + }, "Shop Tile Framework": { "ID": "Cherry.ShopTileFramework", "~1.0.10-alpha-20240227 | Status": "AssumeBroken", "~1.0.10-alpha-20240227 | StatusReasonDetails": "ItemStockInformation references crash at runtime" }, + "Stardew Valley Timelapse": { + "ID": "evskii.StardewTimelapse", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "crashes game due to permission errors when it takes a screenshot" + }, "Swim (FlyingTNT)": { "ID": "FlyingTNT.Swim", "~1.6.4 | Status": "AssumeBroken", "~1.6.4 | StatusReasonDetails": "Harmony patches fail at runtime" }, + "Tile Transparency": { + "ID": "aedenthorn.TileTransparency", + "~0.2.0 | Status": "AssumeBroken", + "~0.2.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "TMXL Map Toolkit": { "ID": "Platonymous.TMXLoader", "~1.24.3-alpha.20240226 | Status": "AssumeBroken", @@ -192,6 +257,21 @@ "Default | UpdateKey": "Nexus:1820" }, + "Tool Assembly": { + "ID": "ofts.toolAss", + "~0.4.1-beta.1 | Status": "AssumeBroken", + "~0.4.1-beta.1 | StatusReasonDetails": "content edits fail at runtime" + }, + "Tool Upgrade Costs": { + "ID": "Cherry.ToolUpgradeCosts", + "~1.0.1-alpha.20240225 | Status": "AssumeBroken", + "~1.0.1-alpha.20240225 | StatusReasonDetails": "ItemStockInformation references crash at runtime" + }, + "Yet Another Fishing Mod": { + "ID": "NeverToxic.YetAnotherFishingMod", + "~1.1.2 | Status": "AssumeBroken", + "~1.1.2 | StatusReasonDetails": "Harmony patches fail at runtime" + }, /********* ** Broke in SDV 1.6 From b83fcc7e7e7dcb4bda84e372c7d484aa4e2122c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Nov 2024 01:27:53 -0500 Subject: [PATCH 10/16] update NuGet packages This mainly fixes a fatal crash when combining MonoMod/Harmony with Pintail-proxied structs to interfaces. --- docs/release-notes.md | 2 ++ src/Directory.Packages.props | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1d4f8c87c..1ffb0a653 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,11 +4,13 @@ ## Upcoming release * For players: * Updated mod compatibility list. + * Fixed crash with some rare combinations of mods involving Harmony and mod APIs. * For mod authors: * Added `PathUtilities.CreateSlug` to turn an arbitrary string into a safe 'slug' that can be used in special contexts like URLs and file paths. _For example, `PathUtilities.CreateSlug("some 例子?!/\\~ text")` becomes `"some-例子-text"`._ * `PathUtilities.IsSlug` is now less strict and allows more Unicode characters. + * Updated [Pintail](https://github.com/Nanoray-pl/Pintail) 2.6.0 → 2.6.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#261)). * For the web UI: * Revamped how the mod compatibility list works to simplify maintenance. diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 7efb352b8..247407d80 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,11 +5,11 @@ - - + + - + @@ -18,14 +18,14 @@ - + - + From fd2648bad8191cf8c778b5a11dece507a99d177c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Nov 2024 01:32:16 -0500 Subject: [PATCH 11/16] fix unit tests --- src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs | 2 +- src/SMAPI/Translation.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index 69cf348ec..4f081c104 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -313,7 +313,7 @@ public bool IsSlug(string input) [TestCase("ex-ample", ExpectedResult = "ex-ample")] [TestCase("ex_ample", ExpectedResult = "ex_ample")] [TestCase("ex.ample", ExpectedResult = "ex.ample")] - [TestCase("ex-ample---text", ExpectedResult = "ex-ample---text")] + [TestCase("ex-ample---text", ExpectedResult = "ex-ample-text")] [TestCase("eXAMple", ExpectedResult = "eXAMple")] [TestCase("example-例子-text", ExpectedResult = "example-例子-text")] diff --git a/src/SMAPI/Translation.cs b/src/SMAPI/Translation.cs index 1ba3e0be0..6eeb50ab9 100644 --- a/src/SMAPI/Translation.cs +++ b/src/SMAPI/Translation.cs @@ -171,7 +171,7 @@ public override string ToString() else if (this.ShouldUsePlaceholder) rawText = string.Format(Translation.PlaceholderText, this.Key); else - rawText = null; + rawText = this.Text; this.CachedResult = this.FormatText(rawText); this.Cached = true; From 6f7996e96275376801ce8ef4f9795444dcd86d33 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Nov 2024 21:42:56 -0500 Subject: [PATCH 12/16] fix typo --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 996f1651c..1043c1c30 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -500,7 +500,7 @@ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relati // not found assetName = null; - error = "The tilesheet couldn't be found relative to either map file or the game's content folder."; + error = "The tilesheet couldn't be found relative to either the map file or the game's content folder."; return false; } From 015c5105984add6a7a1ec018827bb2c102871009 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Nov 2024 21:42:57 -0500 Subject: [PATCH 13/16] fix log parser not highlighting update alerts for non-loaded mods --- docs/release-notes.md | 1 + .../Framework/LogParsing/LogParser.cs | 41 +++++++++++++++---- .../Framework/LogParsing/Models/ModType.cs | 3 ++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1ffb0a653..994d31cf6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * For the web UI: * Revamped how the mod compatibility list works to simplify maintenance. + * Fixed log parser not highlighting update alerts for mods which SMAPI couldn't load. * Fixed CurseForge links not shown for mods that have a CurseForge page. * For external tools: diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 5367ee919..cd35cf3ae 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -41,11 +41,17 @@ public class LogParser /// A regex pattern matching an entry in SMAPI's content pack list. private readonly Regex ContentPackListEntryPattern = new(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))? \| for (?[^\|]*)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + /// A regex pattern matching the start of SMAPI's skipped mods list. + private readonly Regex SkippedModListStartPattern = new("^ Skipped mods$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// A regex pattern matching an entry in SMAPI's skipped mods list. + private readonly Regex SkippedModListEntryPattern = new(@"^ (?:--------------------------------------------------| These mods could not be added to your game\.| - (?.+?) (?[^\s]+)(?: because .+)?)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + /// A regex pattern matching the start of SMAPI's mod update list. private readonly Regex ModUpdateListStartPattern = new(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's mod update list. - private readonly Regex ModUpdateListEntryPattern = new(@"^ (?.+) (?[^\s]+): (?[^\s]+)(?: \(you have [^\)]+\))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModUpdateListEntryPattern = new(@"^ (?.+) (?[^\s]+): (?[^\s]+)(?: \(you have (?[^\)]+)\))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching SMAPI's update line. private readonly Regex SmapiUpdatePattern = new(@"^You can update SMAPI to (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -85,6 +91,7 @@ public ParsedLog Parse(string? logText) IDictionary> mods = new Dictionary>(); bool inModList = false; bool inContentPackList = false; + bool inSkippedModsList = false; bool inModUpdateList = false; foreach (LogMessage message in log.Messages) { @@ -121,6 +128,7 @@ public ParsedLog Parse(string? logText) // update flags inModList = inModList && message.Level == LogLevel.Info && this.ModListEntryPattern.IsMatch(message.Text); inContentPackList = inContentPackList && message.Level == LogLevel.Info && this.ContentPackListEntryPattern.IsMatch(message.Text); + inSkippedModsList = inSkippedModsList && message.Level == LogLevel.Error && this.SkippedModListEntryPattern.IsMatch(message.Text); inModUpdateList = inModUpdateList && message.Level == LogLevel.Alert && this.ModUpdateListEntryPattern.IsMatch(message.Text); // mod list @@ -168,6 +176,24 @@ public ParsedLog Parse(string? logText) message.Section = LogSection.ContentPackList; } + // skipped mods list + if (!inSkippedModsList && message.Level == LogLevel.Error && this.SkippedModListStartPattern.IsMatch(message.Text)) + inSkippedModsList = true; + else if (inSkippedModsList) + { + Match match = this.SkippedModListEntryPattern.Match(message.Text); + + if (match.Groups["name"].Success) + { + string name = match.Groups["name"].Value; + string version = match.Groups["version"].Value; + + if (!mods.TryGetValue(name, out List? entries)) + mods[name] = entries = new List(); + entries.Add(new LogModInfo(ModType.Unknown, name: name, author: "", version: version, description: "", contentPackFor: null, loaded: false)); + } + } + // mod update list else if (!inModUpdateList && message.Level == LogLevel.Alert && this.ModUpdateListStartPattern.IsMatch(message.Text)) { @@ -179,14 +205,15 @@ public ParsedLog Parse(string? logText) { Match match = this.ModUpdateListEntryPattern.Match(message.Text); string name = match.Groups["name"].Value; - string version = match.Groups["version"].Value; + string oldVersion = match.Groups["oldVersion"].Value; + string newVersion = match.Groups["newVersion"].Value; string link = match.Groups["link"].Value; - if (mods.TryGetValue(name, out var entries)) - { - foreach (LogModInfo entry in entries) - entry.SetUpdate(version, link); - } + if (!mods.TryGetValue(name, out var entries)) + mods[name] = entries = [new LogModInfo(ModType.Unknown, name: name, author: "", version: oldVersion, description: "", loaded: false)]; + + foreach (LogModInfo entry in entries) + entry.SetUpdate(newVersion, link); message.Section = LogSection.ModUpdateList; } diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs index c60d19d5e..4eff2d532 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models; /// The type for a instance. public enum ModType { + /// A mod or content pack whose type isn't know (e.g. because it's from the skipped-mods list). + Unknown, + /// A special non-mod entry (e.g. for SMAPI or the game itself). Special, From 72fb9248dfb63b514056cd8a8de70e504371ad02 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Nov 2024 19:47:50 -0500 Subject: [PATCH 14/16] sign SMAPI on install to avoid new macOS security restrictions --- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 994d31cf6..b206b47b6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * Updated mod compatibility list. * Fixed crash with some rare combinations of mods involving Harmony and mod APIs. + * Fixed compatibility with newer macOS security restrictions. * For mod authors: * Added `PathUtilities.CreateSlug` to turn an arbitrary string into a safe 'slug' that can be used in special contexts like URLs and file paths. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index f25f7cfac..4b6544afd 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -415,6 +415,21 @@ public void Run(string[] args) } }.Start(); } + + // sign SMAPI on macOS + // This avoids 'StardewModdingAPI will damage your computer' errors in newer versions. + if (context.Platform is Platform.Mac) + { + new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "codesign", + Arguments = $"--force --sign - \"{paths.UnixSmapiExecutablePath}\"", + CreateNoWindow = true + } + }.Start(); + } } // copy the game's deps.json file From fe5264e2bdefaefc09f56efa932d355bafccd372 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Nov 2024 21:15:39 -0500 Subject: [PATCH 15/16] fetch compatibility list from release branch --- .../Clients/CompatibilityRepo/CompatibilityRepoClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index 2d841b9cf..5fe7c4b9c 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -29,7 +29,7 @@ public class CompatibilityRepoClient : IDisposable /// Construct an instance. /// The full URL of the JSON file to fetch. /// The user agent for the API client. - public CompatibilityRepoClient(string userAgent, string fetchUrl = "https://raw.githubusercontent.com/Pathoschild/SmapiCompatibilityList/refs/heads/develop/data/data.jsonc") + public CompatibilityRepoClient(string userAgent, string fetchUrl = "https://raw.githubusercontent.com/Pathoschild/SmapiCompatibilityList/refs/heads/release/data/data.jsonc") { this.Client = new FluentClient(fetchUrl).SetUserAgent(userAgent); this.MarkdownPipeline = new MarkdownPipelineBuilder() From e8987dc5a5682e179699cbf71c25ec7ad49f5f5c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Nov 2024 22:29:41 -0500 Subject: [PATCH 16/16] prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 17 +++++++++-------- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/build/common.targets b/build/common.targets index efb337e88..1dabf36e3 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed. - 4.1.7 + 4.1.8 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index b206b47b6..b9aa34c92 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,26 +1,27 @@ ← [README](README.md) # Release notes -## Upcoming release +## 4.1.8 +Released 28 November 2024 for Stardew Valley 1.6.14 or later. + * For players: - * Updated mod compatibility list. - * Fixed crash with some rare combinations of mods involving Harmony and mod APIs. + * Updated the mod compatibility blacklist. * Fixed compatibility with newer macOS security restrictions. + * Fixed crash with some rare combinations of mods involving Harmony and mod APIs. * For mod authors: - * Added `PathUtilities.CreateSlug` to turn an arbitrary string into a safe 'slug' that can be used in special contexts like URLs and file paths. + * Added `PathUtilities.CreateSlug` to get a safe Unicode string for use in special contexts like URLs and file paths. _For example, `PathUtilities.CreateSlug("some 例子?!/\\~ text")` becomes `"some-例子-text"`._ - * `PathUtilities.IsSlug` is now less strict and allows more Unicode characters. + * `PathUtilities.IsSlug` now allows more Unicode characters. * Updated [Pintail](https://github.com/Nanoray-pl/Pintail) 2.6.0 → 2.6.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#261)). * For the web UI: - * Revamped how the mod compatibility list works to simplify maintenance. * Fixed log parser not highlighting update alerts for mods which SMAPI couldn't load. * Fixed CurseForge links not shown for mods that have a CurseForge page. * For external tools: - * Updated SMAPI toolkit for the new [mod compatibility repo](https://github.com/Pathoschild/SmapiCompatibilityList), which replaces the former [wiki page](https://stardewvalleywiki.com/Modding:Mod_compatibility). - * Added toolkit method to get the URL from an update key site + mod ID. + * Revamped the mod compatibility list to simplify maintenance. It's now stored [in a Git repo](https://github.com/Pathoschild/SmapiCompatibilityList), which replaces the former [wiki page](https://stardewvalleywiki.com/Modding:Mod_compatibility). + * Added toolkit method to get the URL from an update key's site and mod ID. ## 4.1.7 Released 12 November 2024 for Stardew Valley 1.6.14 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 5540967d0..b9a657ee2 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "4.1.7", + "Version": "4.1.8", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "4.1.7" + "MinimumApiVersion": "4.1.8" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 715dbb40b..c8ab0b4c8 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "4.1.7", + "Version": "4.1.8", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "4.1.7" + "MinimumApiVersion": "4.1.8" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 0eb365577..5faabf388 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -49,7 +49,7 @@ internal static class EarlyConstants internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "4.1.7"; + internal static string RawApiVersion = "4.1.8"; } /// Contains SMAPI's constants and assumptions.