summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-06-19 22:10:15 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-06-19 22:10:15 -0400
commitd401aff3307f6e2e1641610fdd912b572d6b04c1 (patch)
tree361db0c08914b34a58ac985aeacd108c8b932ae0 /src
parent4a05cd09b66a9ec37522aa656ab0814095ab6d23 (diff)
downloadSMAPI-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.cs180
-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.cs18
-rw-r--r--src/SMAPI/Framework/ModData/ParsedModDataRecord.cs7
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs25
-rw-r--r--src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs37
-rw-r--r--src/SMAPI/Program.cs143
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj1
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs30
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs15
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs34
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs39
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