summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md14
-rw-r--r--docs/technical-docs.md43
-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
14 files changed, 312 insertions, 278 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index df832c34..b3ab2481 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -7,6 +7,8 @@
* Added prompt when in beta channel and a new version is found.
* Added friendly error when game can't start audio.
* Added console warning for mods which don't have update checks configured.
+ * Added update checks for optional mod files on Nexus.
+ * Added `player_add name` command, which lets you add items to your inventory by name instead of ID.
* Improved how mod warnings are shown in the console.
* Fixed `SEHException` errors and performance issues in some cases.
* Fixed console color scheme on Mac or in PowerShell, configurable via `StardewModdingAPI.config.json`.
@@ -16,6 +18,8 @@
* Fixed installer not removing some SMAPI files.
* Fixed `smapi.io/install` not linking to a useful page.
* Fixed `world_setseason` command not running season-change logic.
+ * Fixed `world_setseason` command not normalising the season value.
+ * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed).
* Fixed mod update checks failing if a mod only has prerelease versions on GitHub.
* Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!)
* Fixed Nexus mod update alerts not showing HTTPS links.
@@ -38,6 +42,7 @@
* Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map.
* Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.)
* Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`.
+ * Update checks now use the update key order when deciding which to link to.
* Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`).
* Fixed assets loaded by temporary content managers not being editable by mods.
* Fixed assets not reloaded consistently when the player switches language.
@@ -54,14 +59,9 @@
* Mods can't intercept chatbox input.
* Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.)
-* In console commands:
- * Added `player_add name`, which lets you add items to your inventory by name instead of ID.
- * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed).
- * Fixed `world_setseason` not normalising the season value.
-
* For the web UI:
- * Improved log parser design to make it more intuitive.
- * Improved layout on small screens.
+ * Redesigned log parser to make it more intuitive.
+ * Redesigned UI to be more mobile-friendly.
* Added option to download from Nexus.
* Changed log parser filters to show `DEBUG` messages by default.
* Fixed log parser issue when content packs have no description.
diff --git a/docs/technical-docs.md b/docs/technical-docs.md
index f4358e31..bdb731d1 100644
--- a/docs/technical-docs.md
+++ b/docs/technical-docs.md
@@ -161,7 +161,7 @@ The log parser lives at https://log.smapi.io.
### Mods API
The mods API provides version info for mods hosted by Chucklefish, GitHub, or Nexus Mods. It's used
by SMAPI to perform update checks. The `{version}` URL token is the version of SMAPI making the
-request; it doesn't do anything currently, but lets us version breaking changes if needed.
+request, and is used when needed for backwards compatibility.
Each mod is identified by a repository key and unique identifier (like `nexus:541`). The following
repositories are supported:
@@ -173,32 +173,37 @@ key | repository
`nexus` | A mod page on [Nexus Mods](https://www.nexusmods.com/stardewvalley), identified by the mod ID in the page URL.
-The API accepts either `GET` or `POST` for convenience:
-> ```
->GET https://api.smapi.io/v2.0/mods?modKeys=nexus:541,chucklefish:4228
->```
-
+The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and
+update keys.
>```
>POST https://api.smapi.io/v2.0/mods
>{
-> "ModKeys": [ "nexus:541", "chucklefish:4228" ]
+> "mods": [
+> {
+> "id": "Pathoschild.LookupAnything",
+> "updateKeys": [ "nexus:541", "chucklefish:4250" ]
+> }
+> ]
>}
>```
-It returns a response like this:
+The API will automatically aggregate versions and errors, and return a response like this. The
+latest version is the main mod version (e.g. 'latest version' field on Nexus); if available and
+newer, the latest optional version will be shown as the 'preview version'.
>```
>{
-> "chucklefish:4228": {
-> "name": "Entoarox Framework",
-> "version": "1.8.0",
-> "url": "https://community.playstarbound.com/resources/4228"
-> },
-> "nexus:541": {
-> "name": "Lookup Anything",
-> "version": "1.16",
-> "url": "http://www.nexusmods.com/stardewvalley/mods/541"
-> }
->}
+> "Pathoschild.LookupAnything": {
+> "id": "Pathoschild.LookupAnything",
+> "name": "Lookup Anything",
+> "version": "1.18",
+> "url": "https://www.nexusmods.com/stardewvalley/mods/541",
+> "previewVersion": "1.19-beta",
+> "previewUrl": "https://www.nexusmods.com/stardewvalley/mods/541",
+> "errors": [
+> "The update key 'chucklefish:4250' matches a mod with invalid semantic version '*'."
+> ]
+> }
+}
>```
## Development
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