diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-06-19 22:10:15 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-06-19 22:10:15 -0400 |
commit | d401aff3307f6e2e1641610fdd912b572d6b04c1 (patch) | |
tree | 361db0c08914b34a58ac985aeacd108c8b932ae0 /src | |
parent | 4a05cd09b66a9ec37522aa656ab0814095ab6d23 (diff) | |
download | SMAPI-d401aff3307f6e2e1641610fdd912b572d6b04c1.tar.gz SMAPI-d401aff3307f6e2e1641610fdd912b572d6b04c1.tar.bz2 SMAPI-d401aff3307f6e2e1641610fdd912b572d6b04c1.zip |
rewrite update checks (#551)
Diffstat (limited to 'src')
-rw-r--r-- | src/SMAPI.Web/Controllers/ModsApiController.cs | 180 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs (renamed from src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/IModMetadata.cs | 18 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModData/ParsedModDataRecord.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadata.cs | 25 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs | 37 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 143 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 1 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs | 30 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs | 15 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs | 34 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs | 39 |
12 files changed, 281 insertions, 252 deletions
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1ec855d5..c5a1705d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -67,70 +68,89 @@ namespace StardewModdingAPI.Web.Controllers } /// <summary>Fetch version metadata for the given mods.</summary> - /// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param> - /// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param> - [HttpGet] - public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys, bool allowInvalidVersions = false) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary<string, ModInfoModel>(); - - return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions)); - } - - /// <summary>Fetch version metadata for the given mods.</summary> - /// <param name="search">The mod search criteria.</param> + /// <param name="model">The mod search criteria.</param> [HttpPost] - public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search) + public async Task<IDictionary<string, ModEntryModel>> PostAsync([FromBody] ModSearchModel model) { - // parse model - bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary<string, ModInfoModel> result = new Dictionary<string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) + ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase); + foreach (ModSearchEntryModel mod in searchMods) { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + if (string.IsNullOrWhiteSpace(mod.ID)) continue; - } - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + // get latest versions + ModEntryModel result = new ModEntryModel { ID = mod.ID }; + IList<string> errors = new List<string>(); + foreach (string updateKey in mod.UpdateKeys ?? new string[0]) { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) + { + errors.Add(data.Error); + continue; + } - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - // fetch info - ModInfoModel info = await repository.GetModInfoAsync(modID); + // handle main version + if (data.Version != null) + { + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); + continue; + } + + if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + { + result.Name = data.Name; + result.Url = data.Url; + result.Version = version.ToString(); + } + } - // validate - if (info.Error == null) + // handle optional version + if (data.PreviewVersion != null) { - if (info.Version == null) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); - if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; + } + + if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + { + result.Name = result.Name ?? data.Name; + result.PreviewUrl = data.Url; + result.PreviewVersion = version.ToString(); + } } + } + + // fallback to preview if latest is invalid + if (result.Version == null && result.PreviewVersion != null) + { + result.Version = result.PreviewVersion; + result.Url = result.PreviewUrl; + result.PreviewVersion = null; + result.PreviewUrl = null; + } + + // special cases + if (mod.ID == "Pathoschild.SMAPI") + { + result.Name = "SMAPI"; + result.Url = "https://smapi.io/"; + if (result.PreviewUrl != null) + result.PreviewUrl = "https://smapi.io/"; + } - // cache & return - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); - return info; - }); + // add result + result.Errors = errors.ToArray(); + mods[mod.ID] = result; } - return result; + return mods; } @@ -158,5 +178,63 @@ namespace StardewModdingAPI.Web.Controllers modID = parts[1].Trim(); return true; } + + /// <summary>Get the mods for which the API should return data.</summary> + /// <param name="model">The search model.</param> + private IEnumerable<ModSearchEntryModel> GetSearchMods(ModSearchModel model) + { + if (model == null) + yield break; + + // yield standard entries + if (model.Mods != null) + { + foreach (ModSearchEntryModel mod in model.Mods) + yield return mod; + } + + // yield mod update keys if backwards compatible + if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + { + foreach (string updateKey in model.ModKeys.Distinct()) + yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); + } + } + + /// <summary>Get the mod info for an update key.</summary> + /// <param name="updateKey">The namespaced update key.</param> + private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(string updateKey) + { + // parse update key + if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) + return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + + // fetch mod info + return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + ModInfoModel result = await repository.GetModInfoAsync(modID); + if (result.Error != null) + { + if (result.Version == null) + result.Error = $"The update key '{updateKey}' matches a mod with no version number."; + else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; + } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); + return result; + }); + } + + /// <summary>Get whether the API should return data in a backwards compatible way.</summary> + /// <param name="maxVersion">The last version for which data should be backwards compatible.</param> + private bool ShouldBeBackwardsCompatible(string maxVersion) + { + string actualVersion = (string)this.RouteData.Values["version"]; + return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs index c8e296f0..ccb0699c 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +namespace StardewModdingAPI.Web.Framework.ModRepositories { /// <summary>Generic metadata about a mod.</summary> public class ModInfoModel @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Version = version; this.PreviewVersion = previewVersion; this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser + this.Error = error; } /// <summary>Construct an instance.</summary> diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index b71c8056..d3ec0035 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,6 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework { @@ -46,11 +46,9 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the mod is a content pack.</summary> bool IsContentPack { get; } - /// <summary>The update status of this mod (if any).</summary> - ModUpdateStatus UpdateStatus { get; } + /// <summary>The update-check metadata for this mod (if any).</summary> + ModEntryModel UpdateCheckData { get; } - /// <summary>The preview update status of this mod (if any).</summary> - ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -78,13 +76,9 @@ namespace StardewModdingAPI.Framework /// <param name="api">The mod-provided API.</param> IModMetadata SetApi(object api); - /// <summary>Set the update status.</summary> - /// <param name="updateStatus">The mod update status.</param> - IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); - - /// <summary>Set the preview update status.</summary> - /// <param name="previewUpdateStatus">The mod preview update status.</param> - IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); + /// <summary>Set the update-check metadata for this mod.</summary> + /// <param name="data">The update-check metadata.</param> + IModMetadata SetUpdateData(ModEntryModel data); /// <summary>Whether the mod manifest was loaded (regardless of whether the mod itself was loaded).</summary> bool HasManifest(); diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index deb12bdc..3801fac3 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -40,9 +40,12 @@ namespace StardewModdingAPI.Framework.ModData /// <summary>Get a semantic remote version for update checks.</summary> /// <param name="version">The remote version to normalise.</param> - public string GetRemoteVersionForUpdateChecks(string version) + public ISemanticVersion GetRemoteVersionForUpdateChecks(string version) { - return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version); + return rawVersion != null + ? new SemanticVersion(rawVersion) + : null; } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 88d2770c..02a77778 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; -using StardewModdingAPI.Framework.ModUpdateChecking; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; namespace StardewModdingAPI.Framework.ModLoading { @@ -44,11 +44,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The mod-provided API (if any).</summary> public object Api { get; private set; } - /// <summary>The update status of this mod (if any).</summary> - public ModUpdateStatus UpdateStatus { get; private set; } - - /// <summary>The preview update status of this mod (if any).</summary> - public ModUpdateStatus PreviewUpdateStatus { get; private set; } + /// <summary>The update-check metadata for this mod (if any).</summary> + public ModEntryModel UpdateCheckData { get; private set; } /// <summary>Whether the mod is a content pack.</summary> public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -122,19 +119,11 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// <summary>Set the update status.</summary> - /// <param name="updateStatus">The mod update status.</param> - public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) - { - this.UpdateStatus = updateStatus; - return this; - } - - /// <summary>Set the preview update status.</summary> - /// <param name="previewUpdateStatus">The mod preview update status.</param> - public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) + /// <summary>Set the update-check metadata for this mod.</summary> + /// <param name="data">The update-check metadata.</param> + public IModMetadata SetUpdateData(ModEntryModel data) { - this.PreviewUpdateStatus = previewUpdateStatus; + this.UpdateCheckData = data; return this; } diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs deleted file mode 100644 index efb32aef..00000000 --- a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace StardewModdingAPI.Framework.ModUpdateChecking -{ - /// <summary>Update status for a mod.</summary> - internal class ModUpdateStatus - { - /********* - ** Accessors - *********/ - /// <summary>The version that this mod can be updated to (if any).</summary> - public ISemanticVersion Version { get; } - - /// <summary>The error checking for updates of this mod (if any).</summary> - public string Error { get; } - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="version">The version that this mod can be update to.</param> - public ModUpdateStatus(ISemanticVersion version) - { - this.Version = version; - } - - /// <summary>Construct an instance.</summary> - /// <param name="error">The error checking for updates of this mod.</param> - public ModUpdateStatus(string error) - { - this.Error = error; - } - - /// <summary>Construct an instance.</summary> - public ModUpdateStatus() - { - } - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2ee18a29..ccdf98ef 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -8,6 +8,7 @@ using System.Net; using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Xna.Framework.Input; @@ -24,7 +25,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -592,14 +592,14 @@ namespace StardewModdingAPI ISemanticVersion updateFound = null; try { - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + ModEntryModel response = client.GetModInfo(new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" })).Single().Value; ISemanticVersion latestStable = response.Version != null ? new SemanticVersion(response.Version) : null; ISemanticVersion latestBeta = response.PreviewVersion != null ? new SemanticVersion(response.PreviewVersion) : null; - if (response.Error != null) + if (latestStable == null && response.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {response.Error}"); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) { @@ -634,103 +634,72 @@ namespace StardewModdingAPI { HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); - // prepare update keys - Dictionary<string, IModMetadata[]> modsByKey = - ( - from mod in mods - where - mod.Manifest?.UpdateKeys != null - && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // fetch results - this.Monitor.Log($" Checking {modsByKey.Count} mod update keys.", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary<IModMetadata, Tuple<ModInfoModel, bool>> updatesByMod = new Dictionary<IModMetadata, Tuple<ModInfoModel, bool>>(); - foreach (var result in results) + // prepare search model + List<ModSearchEntryModel> searchMods = new List<ModSearchEntryModel>(); + foreach (IModMetadata mod in mods) { - IModMetadata mod = result.Mod; - ModInfoModel remoteInfo = result.Info; - - // handle error - if (remoteInfo.Error != null) - { - if (mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - if (mod.PreviewUpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); - - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); + if (!mod.HasManifest()) continue; - } - // normalise versions - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); - bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + string[] updateKeys = mod.Manifest.UpdateKeys ?? new string[0]; + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.Except(suppressUpdateChecks).ToArray())); + } - if (!validVersion && mod.UpdateStatus?.Version == null) - mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); - if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) - mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary<string, ModEntryModel> results = client.GetModInfo(searchMods.ToArray()); - if (!validVersion && !validPreviewVersion) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); + // extract update alerts & errors + var updates = new List<Tuple<IModMetadata, ISemanticVersion, string>>(); + var errors = new StringBuilder(); + foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) + { + // link to update-check data + if (!mod.HasManifest() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result)) continue; - } - - // compare versions - bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); - bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; + mod.SetUpdateData(result); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); - if (isUpdate) + // handle errors + if (result.Errors != null && result.Errors.Any()) { - if (!updatesByMod.TryGetValue(mod, out Tuple<ModInfoModel, bool> other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) - { - updatesByMod[mod] = new Tuple<ModInfoModel, bool>(remoteInfo, isPreviewUpdate); - - if (isPreviewUpdate) - mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); - else - mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); - } + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName} update error: {result.Errors[0]}" + : $" {mod.DisplayName} update errors:\n - {string.Join("\n - ", result.Errors)}" + ); } - } - // set mods to have no updates - foreach (IModMetadata mod in results.Select(item => item.Mod) - .Where(item => !updatesByMod.ContainsKey(item))) - { - mod.SetUpdateStatus(new ModUpdateStatus()); - mod.SetPreviewUpdateStatus(new ModUpdateStatus()); + // parse versions + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + ISemanticVersion latestVersion = result.Version != null + ? mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Version) ?? new SemanticVersion(result.Version) + : null; + ISemanticVersion optionalVersion = result.PreviewVersion != null + ? (mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.PreviewVersion) ?? new SemanticVersion(result.PreviewVersion)) + : null; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Url)); + else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) + updates.Add(Tuple.Create(mod, optionalVersion, result.Url)); } - // output - if (updatesByMod.Any()) + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Encountered errors fetching updates for some mods:\n" + errors.ToString(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) { this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {(entry.Value.Item2 ? entry.Value.Item1.PreviewVersion : entry.Value.Item1.Version)}: {entry.Value.Item1.Url}", LogLevel.Alert); + this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updates) + { + IModMetadata mod = entry.Item1; + ISemanticVersion newVersion = entry.Item2; + string newUrl = entry.Item3; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } } else this.Monitor.Log(" All mods up to date.", LogLevel.Trace); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 916dd053..fcd54c34 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,7 +110,6 @@ <Compile Include="Framework\ContentManagers\IContentManager.cs" /> <Compile Include="Framework\ContentManagers\ModContentManager.cs" /> <Compile Include="Framework\Models\ModFolderExport.cs" /> - <Compile Include="Framework\ModUpdateChecking\ModUpdateStatus.cs" /> <Compile Include="Framework\Patching\GamePatcher.cs" /> <Compile Include="Framework\Patching\IHarmonyPatch.cs" /> <Compile Include="Framework\Serialisation\ColorConverter.cs" /> diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs new file mode 100644 index 00000000..0f268231 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// <summary>Metadata about a mod.</summary> + public class ModEntryModel + { + /********* + ** Accessors + *********/ + /// <summary>The mod's unique ID (if known).</summary> + public string ID { get; set; } + + /// <summary>The mod name.</summary> + public string Name { get; set; } + + /// <summary>The mod's latest release number.</summary> + public string Version { get; set; } + + /// <summary>The mod's web URL.</summary> + public string Url { get; set; } + + /// <summary>The mod's latest optional release, if newer than <see cref="Version"/>.</summary> + public string PreviewVersion { get; set; } + + /// <summary>The web URL to the mod's latest optional release, if newer than <see cref="Version"/>.</summary> + public string PreviewUrl { get; set; } + + /// <summary>The errors that occurred while fetching update data.</summary> + public string[] Errors { get; set; } = new string[0]; + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index c0ee34ea..ffca32ca 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi @@ -10,10 +10,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// <summary>The namespaced mod keys to search.</summary> + [Obsolete] public string[] ModKeys { get; set; } - /// <summary>Whether to allow non-semantic versions, instead of returning an error for those.</summary> - public bool AllowInvalidVersions { get; set; } + /// <summary>The mods for which to find data.</summary> + public ModSearchEntryModel[] Mods { get; set; } /********* @@ -26,12 +27,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi } /// <summary>Construct an instance.</summary> - /// <param name="modKeys">The namespaced mod keys to search.</param> - /// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param> - public ModSearchModel(IEnumerable<string> modKeys, bool allowInvalidVersions) + /// <param name="mods">The mods to search.</param> + public ModSearchModel(ModSearchEntryModel[] mods) { - this.ModKeys = modKeys.ToArray(); - this.AllowInvalidVersions = allowInvalidVersions; + this.Mods = mods.ToArray(); } } } diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs new file mode 100644 index 00000000..bca47647 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// <summary>Specifies the identifiers for a mod to match.</summary> + public class ModSearchEntryModel + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod ID.</summary> + public string ID { get; set; } + + /// <summary>The namespaced mod update keys (if available).</summary> + public string[] UpdateKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + public ModSearchEntryModel() + { + // needed for JSON deserialising + } + + /// <summary>Construct an instance.</summary> + /// <param name="id">The unique mod ID.</param> + /// <param name="updateKeys">The namespaced mod update keys (if available).</param> + public ModSearchEntryModel(string id, string[] updateKeys) + { + this.ID = id; + this.UpdateKeys = updateKeys ?? new string[0]; + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index d94b0259..892dfeba 100644 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using Newtonsoft.Json; @@ -31,44 +30,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Version = version; } - /// <summary>Get the latest SMAPI version.</summary> - /// <param name="modKeys">The mod keys for which to fetch the latest version.</param> - public IDictionary<string, ModInfoModel> GetModInfo(params string[] modKeys) + /// <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> + public IDictionary<string, ModEntryModel> GetModInfo(params ModSearchEntryModel[] mods) { - return this.Post<ModSearchModel, Dictionary<string, ModInfoModel>>( + return this.Post<ModSearchModel, Dictionary<string, ModEntryModel>>( $"v{this.Version}/mods", - new ModSearchModel(modKeys, allowInvalidVersions: true) + new ModSearchModel(mods) ); } - /// <summary>Get the latest version for a mod.</summary> - /// <param name="updateKeys">The update keys to search.</param> - public ISemanticVersion GetLatestVersion(string[] updateKeys) - { - if (!updateKeys.Any()) - return null; - - // fetch update results - ModInfoModel[] results = this - .GetModInfo(updateKeys) - .Values - .Where(p => p.Error == null) - .ToArray(); - if (!results.Any()) - return null; - - ISemanticVersion latest = null; - foreach (ModInfoModel result in results) - { - if (!SemanticVersion.TryParse(result.PreviewVersion ?? result.Version, out ISemanticVersion cur)) - continue; - - if (latest == null || cur.IsNewerThan(latest)) - latest = cur; - } - return latest; - } - /********* ** Private methods |