diff options
Diffstat (limited to 'src')
15 files changed, 276 insertions, 239 deletions
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 8a9c0a25..f1bcfccc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// <summary>Metadata about a mod.</summary> @@ -9,23 +11,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>The mod's unique ID (if known).</summary> public string ID { get; set; } + /// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary> + public ModEntryVersionModel SuggestedUpdate { get; set; } + + /// <summary>Optional extended data which isn't needed for update checks.</summary> + public ModExtendedMetadataModel Metadata { get; set; } + /// <summary>The main version.</summary> + [Obsolete] public ModEntryVersionModel Main { get; set; } /// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary> + [Obsolete] public ModEntryVersionModel Optional { get; set; } /// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary> + [Obsolete] public ModEntryVersionModel Unofficial { get; set; } /// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary> + [Obsolete] public ModEntryVersionModel UnofficialForBeta { get; set; } - /// <summary>Optional extended data which isn't needed for update checks.</summary> - public ModExtendedMetadataModel Metadata { get; set; } - /// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary> - public bool HasBetaInfo { get; set; } + [Obsolete] + public bool? HasBetaInfo { get; set; } /// <summary>The errors that occurred while fetching update data.</summary> public string[] Errors { get; set; } = new string[0]; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 8074210c..4a697585 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -46,6 +46,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>The custom mod page URL (if applicable).</summary> public string CustomUrl { get; set; } + /// <summary>The main version.</summary> + public ModEntryVersionModel Main { get; set; } + + /// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary> + public ModEntryVersionModel Optional { get; set; } + + /// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary> + public ModEntryVersionModel Unofficial { get; set; } + + /// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary> + public ModEntryVersionModel UnofficialForBeta { get; set; } /**** ** Stable compatibility @@ -60,7 +71,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary> public string BrokeIn { get; set; } - /**** ** Beta compatibility ****/ @@ -84,8 +94,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>Construct an instance.</summary> /// <param name="wiki">The mod metadata from the wiki (if available).</param> /// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param> - public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) + /// <param name="main">The main version.</param> + /// <param name="optional">The latest optional version, if newer than <paramref name="main"/>.</param> + /// <param name="unofficial">The latest unofficial version, if newer than <paramref name="main"/> and <paramref name="optional"/>.</param> + /// <param name="unofficialForBeta">The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</param> + public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta) { + // versions + this.Main = main; + this.Optional = optional; + this.Unofficial = unofficial; + this.UnofficialForBeta = unofficialForBeta; + // wiki data if (wiki != null) { diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index 886cd5a1..bf81e102 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -12,6 +12,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>The namespaced mod update keys (if available).</summary> public string[] UpdateKeys { get; set; } + /// <summary>The mod version installed by the local player. This is used for version mapping in some cases.</summary> + public ISemanticVersion InstalledVersion { get; set; } + + /// <summary>Whether the installed version is broken or could not be loaded.</summary> + public bool IsBroken { get; set; } + /********* ** Public methods @@ -24,10 +30,13 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>Construct an instance.</summary> /// <param name="id">The unique mod ID.</param> + /// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param> /// <param name="updateKeys">The namespaced mod update keys (if available).</param> - public ModSearchEntryModel(string id, string[] updateKeys) + /// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param> + public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false) { this.ID = id; + this.InstalledVersion = installedVersion; this.UpdateKeys = updateKeys ?? new string[0]; } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs index a2eaad16..73698173 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -1,4 +1,5 @@ using System.Linq; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -14,6 +15,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>Whether to include extended metadata for each mod.</summary> public bool IncludeExtendedMetadata { get; set; } + /// <summary>The SMAPI version installed by the player. This is used for version mapping in some cases.</summary> + public ISemanticVersion ApiVersion { get; set; } + + /// <summary>The Stardew Valley version installed by the player.</summary> + public ISemanticVersion GameVersion { get; set; } + + /// <summary>The OS on which the player plays.</summary> + public Platform? Platform { get; set; } + /********* ** Public methods @@ -26,10 +36,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>Construct an instance.</summary> /// <param name="mods">The mods to search.</param> + /// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param> + /// <param name="gameVersion">The Stardew Valley version installed by the player.</param> + /// <param name="platform">The OS on which the player plays.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> - public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) + public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata) { this.Mods = mods.ToArray(); + this.ApiVersion = apiVersion; + this.GameVersion = gameVersion; + this.Platform = platform; this.IncludeExtendedMetadata = includeExtendedMetadata; } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 80c8f62b..f0a7c82a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -37,12 +38,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <summary>Get metadata about a set of mods from the web API.</summary> /// <param name="mods">The mod keys for which to fetch the latest version.</param> + /// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param> + /// <param name="gameVersion">The Stardew Valley version installed by the player.</param> + /// <param name="platform">The OS on which the player plays.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> - public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) + public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false) { return this.Post<ModSearchModel, ModEntryModel[]>( $"v{this.Version}/mods", - new ModSearchModel(mods, includeExtendedMetadata) + new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata) ).ToDictionary(p => p.ID); } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 610e14f1..384f23fc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -102,6 +102,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string anchor = this.GetAttribute(node, "id"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string devNote = this.GetAttribute(node, "data-dev-note"); + IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions"); + IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo @@ -159,6 +161,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Warnings = warnings, MetadataLinks = metadataLinks.ToArray(), DevNote = devNote, + MapLocalVersions = mapLocalVersions, + MapRemoteVersions = mapRemoteVersions, Anchor = anchor }; } @@ -223,6 +227,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki return null; } + /// <summary>Get an attribute value and parse it as a version mapping.</summary> + /// <param name="element">The element whose attributes to read.</param> + /// <param name="name">The attribute name.</param> + private IDictionary<string, string> GetAttributeAsVersionMapping(HtmlNode element, string name) + { + // get raw value + string raw = this.GetAttribute(element, name); + if (raw?.Contains("→") != true) + return null; + + // parse + // Specified on the wiki in the form "remote version → mapped version; another remote version → mapped version" + IDictionary<string, string> map = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); + foreach (string pair in raw.Split(';')) + { + string[] versions = pair.Split('→'); + if (versions.Length == 2 && !string.IsNullOrWhiteSpace(versions[0]) && !string.IsNullOrWhiteSpace(versions[1])) + map[versions[0].Trim()] = versions[1].Trim(); + } + return map; + } + /// <summary>Get the text of an element with the given class name.</summary> /// <param name="container">The metadata container.</param> /// <param name="className">The field name.</param> diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 51bb2336..931dcd43 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { @@ -62,6 +63,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary> public string DevNote { get; set; } + /// <summary>Maps local versions to a semantic version for update checks.</summary> + public IDictionary<string, string> MapLocalVersions { get; set; } + + /// <summary>Maps remote versions to a semantic version for update checks.</summary> + public IDictionary<string, string> MapRemoteVersions { get; set; } + /// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary> public string Anchor { get; set; } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index dd0bd07b..8b40c301 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -25,12 +25,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// </remarks> public string FormerIDs { get; set; } - /// <summary>Maps local versions to a semantic version for update checks.</summary> - public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>(); - - /// <summary>Maps remote versions to a semantic version for update checks.</summary> - public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>(); - /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> public ModWarning SuppressWarnings { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index f01ada7c..c892d820 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -22,12 +22,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> public ModWarning SuppressWarnings { get; set; } - /// <summary>Maps local versions to a semantic version for update checks.</summary> - public IDictionary<string, string> MapLocalVersions { get; } - - /// <summary>Maps remote versions to a semantic version for update checks.</summary> - public IDictionary<string, string> MapRemoteVersions { get; } - /// <summary>The versioned field data.</summary> public ModDataField[] Fields { get; } @@ -44,8 +38,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData this.ID = model.ID; this.FormerIDs = model.GetFormerIDs().ToArray(); this.SuppressWarnings = model.SuppressWarnings; - this.MapLocalVersions = new Dictionary<string, string>(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); - this.MapRemoteVersions = new Dictionary<string, string>(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); this.Fields = model.GetFields().ToArray(); } @@ -67,29 +59,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData return false; } - /// <summary>Get a semantic local version for update checks.</summary> - /// <param name="version">The remote version to normalize.</param> - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// <summary>Get a semantic remote version for update checks.</summary> - /// <param name="version">The remote version to normalize.</param> - public string GetRemoteVersionForUpdateChecks(string version) - { - // normalize version if possible - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) - version = parsed.ToString(); - - // fetch remote version - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - /// <summary>Get the possible mod IDs.</summary> public IEnumerable<string> GetIDs() { diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 9e22990d..598da66a 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -26,29 +26,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// <summary>Get a semantic local version for update checks.</summary> - /// <param name="version">The remote version to normalize.</param> - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// <summary>Get a semantic remote version for update checks.</summary> - /// <param name="version">The remote version to normalize.</param> - public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version) - { - if (version == null) - return null; - - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString()); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : version; - } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index f65b164f..fe220eb5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Web.Controllers new IModRepository[] { new ChucklefishRepository(chucklefish), - new CurseForgeRepository(curseForge), + new CurseForgeRepository(curseForge), new GitHubRepository(github), new ModDropRepository(modDrop), new NexusRepository(nexus) @@ -90,12 +90,15 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>Fetch version metadata for the given mods.</summary> /// <param name="model">The mod search criteria.</param> + /// <param name="version">The requested API version.</param> [HttpPost] - public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model) + public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version) { if (model?.Mods == null) return new ModEntryModel[0]; + bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109"); + // fetch wiki data WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase); @@ -104,7 +107,25 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion); + if (legacyMode) + { + result.Main = result.Metadata.Main; + result.Optional = result.Metadata.Optional; + result.Unofficial = result.Metadata.Unofficial; + result.UnofficialForBeta = result.Metadata.UnofficialForBeta; + result.HasBetaInfo = result.Metadata.BetaCompatibilityStatus != null; + result.SuggestedUpdate = null; + if (!model.IncludeExtendedMetadata) + result.Metadata = null; + } + else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) + { + var errors = new List<string>(result.Errors); + errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage."); + result.Errors = errors.ToArray(); + } + mods[mod.ID] = result; } @@ -120,8 +141,9 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="search">The mod data to match.</param> /// <param name="wikiData">The wiki data.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> + /// <param name="apiVersion">The SMAPI version installed by the player.</param> /// <returns>Returns the mod data if found, else <c>null</c>.</returns> - private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) + private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion) { // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); @@ -131,6 +153,10 @@ namespace StardewModdingAPI.Web.Controllers // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList<string> errors = new List<string>(); + ModEntryVersionModel main = null; + ModEntryVersionModel optional = null; + ModEntryVersionModel unofficial = null; + ModEntryVersionModel unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // validate update key @@ -151,76 +177,118 @@ namespace StardewModdingAPI.Web.Controllers // handle main version if (data.Version != null) { - if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions); + if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } - if (this.IsNewer(version, result.Main?.Version)) - result.Main = new ModEntryVersionModel(version, data.Url); + if (this.IsNewer(version, main?.Version)) + main = new ModEntryVersionModel(version, data.Url); } // handle optional version if (data.PreviewVersion != null) { - if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions); + if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); continue; } - if (this.IsNewer(version, result.Optional?.Version)) - result.Optional = new ModEntryVersionModel(version, data.Url); + if (this.IsNewer(version, optional?.Version)) + optional = new ModEntryVersionModel(version, data.Url); } } // get unofficial version - if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); + 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.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { - result.HasBetaInfo = true; if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { - result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) + 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.CompatibilityPageUrl}/#{wikiEntry.Anchor}") : null; } else - result.UnofficialForBeta = result.Unofficial; + unofficialForBeta = unofficial; } } // fallback to preview if latest is invalid - if (result.Main == null && result.Optional != null) + if (main == null && optional != null) { - result.Main = result.Optional; - result.Optional = null; + main = optional; + optional = null; } // special cases if (result.ID == "Pathoschild.SMAPI") { - if (result.Main != null) - result.Main.Url = "https://smapi.io/"; - if (result.Optional != null) - result.Optional.Url = "https://smapi.io/"; + if (main != null) + main.Url = "https://smapi.io/"; + if (optional != null) + optional.Url = "https://smapi.io/"; + } + + // get recommended update (if any) + ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions); + if (apiVersion != null && installedVersion != null) + { + // get newer versions + List<ModEntryVersionModel> updates = new List<ModEntryVersionModel>(); + if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true)) + updates.Add(main); + if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease())) + updates.Add(optional); + if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken)) + updates.Add(unofficial); + if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) + updates.Add(unofficialForBeta); + + // get newest version + ModEntryVersionModel newest = null; + foreach (ModEntryVersionModel update in updates) + { + if (newest == null || update.Version.IsNewerThan(newest.Version)) + newest = update; + } + + // set field + result.SuggestedUpdate = newest != null + ? new ModEntryVersionModel(newest.Version, newest.Url) + : null; } // add extended metadata - if (includeExtendedMetadata && (wikiEntry != null || record != null)) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + if (includeExtendedMetadata) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta); // add result result.Errors = errors.ToArray(); return result; } + /// <summary>Get whether a given version should be offered to the user as an update.</summary> + /// <param name="currentVersion">The current semantic version.</param> + /// <param name="newVersion">The target semantic version.</param> + /// <param name="useBetaChannel">Whether the user enabled the beta channel and should be offered prerelease updates.</param> + private bool IsRecommendedUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) + { + return + newVersion != null + && newVersion.IsNewerThan(currentVersion) + && (useBetaChannel || !newVersion.IsPrerelease()); + } + /// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary> /// <param name="current">The current version.</param> /// <param name="other">The other version.</param> @@ -260,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="specifiedKeys">The specified update keys.</param> /// <param name="record">The mod's entry in SMAPI's internal database.</param> /// <param name="entry">The mod's entry in the wiki list.</param> - public IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { IEnumerable<string> GetRaw() { @@ -301,5 +369,49 @@ namespace StardewModdingAPI.Web.Controllers yield return key; } } + + /// <summary>Get a semantic local version for update checks.</summary> + /// <param name="version">The version to parse.</param> + /// <param name="map">A map of version replacements.</param> + private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map) + { + // try mapped version + string rawNewVersion = this.GetRawMappedVersion(version, map); + if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew)) + return parsedNew; + + // return original version + return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld) + ? parsedOld + : null; + } + + /// <summary>Get a semantic local version for update checks.</summary> + /// <param name="version">The version to map.</param> + /// <param name="map">A map of version replacements.</param> + private string GetRawMappedVersion(string version, IDictionary<string, string> map) + { + if (version == null || map == null || !map.Any()) + return version; + + // match exact raw version + if (map.ContainsKey(version)) + return map[version]; + + |
