overhaul update checks

This commit moves the core update-check logic serverside, and adds support for community-defined version mappings. For example, that means false update alerts can now be solved by the community for all players.
This commit is contained in:
Jesse Plamondon-Willard 2019-11-09 21:18:06 -05:00
parent 01db5e364d
commit fd6a719b02
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
17 changed files with 466 additions and 278 deletions

View File

@ -18,6 +18,9 @@ For players:
* **Improved mod scanning.** * **Improved mod scanning.**
SMAPI now supports some non-standard mod structures automatically, improves compatibility with the Vortex mod manager, and improves various error/skip messages related to mod loading. SMAPI now supports some non-standard mod structures automatically, improves compatibility with the Vortex mod manager, and improves various error/skip messages related to mod loading.
* **Overhauled update checks.**
SMAPI update checks are now handled entirely on the web server and support community-defined version mappings. For example, that means false update alerts can now be solved by the community for all players.
* **Fixed many bugs and edge cases.** * **Fixed many bugs and edge cases.**
For modders: For modders:
@ -50,6 +53,7 @@ For modders:
* Added update checks for CurseForge mods. * Added update checks for CurseForge mods.
* Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles). * Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles).
* Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility. * Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility.
* Overhauled update checks and added community-defined version mapping.
* Improved launch script compatibility on Linux (thanks to kurumushi and toastal!). * Improved launch script compatibility on Linux (thanks to kurumushi and toastal!).
* Made error messages more user-friendly in some cases. * Made error messages more user-friendly in some cases.
* Save Backup now works in the background, to avoid affecting startup time for players with a large number of saves. * Save Backup now works in the background, to avoid affecting startup time for players with a large number of saves.

View File

@ -116,54 +116,201 @@ SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools.
accessible but not officially released; it may change at any time. accessible but not officially released; it may change at any time.
### `/mods` endpoint ### `/mods` endpoint
The API has one `/mods` endpoint. This provides mod info, including official versions and URLs The API has one `/mods` endpoint. This crossreferences the mod against a variety of sources (e.g.
(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata the wiki, Chucklefish, CurseForge, ModDrop, and Nexus) to provide metadata mainly intended for
from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by update checks.
external tools to fetch mod data.
The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and The API accepts a `POST` request with these fields:
may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks).
The API will automatically try to fetch known update keys from the wiki and internal data based on
the given ID.
``` <table>
POST https://api.smapi.io/v2.0/mods <tr>
<th>field</th>
<th>summary</th>
</tr>
<tr>
<td><code>mods</code></td>
<td>
The mods for which to fetch metadata. Included fields:
field | summary
----- | -------
`id` | The unique ID in the mod's `manifest.json`. This is used to crossreference with the wiki, and to index mods in the response. If it's unknown (e.g. you just have an update key), you can use a unique fake ID like `FAKE.Nexus.2400`.
`updateKeys` | _(optional)_ [Update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks) which specify the mod pages to check, in addition to any mod pages linked to the `ID`.
`installedVersion` | _(optional)_ The installed version of the mod. If not specified, the API won't recommend an update.
`isBroken` | _(optional)_ Whether SMAPI failed to load the installed version of the mod, e.g. due to incompatibility. If true, the web API will be more permissive when recommending updates (e.g. allowing a stable → prerelease update).
</td>
</tr>
<tr>
<td><code>apiVersion</code></td>
<td>
_(optional)_ The installed version of SMAPI. If not specified, the API won't recommend an update.
</td>
</tr>
<tr>
<td><code>gameVersion</code></td>
<td>
_(optional)_ The installed version of Stardew Valley. This may be used to select updates.
</td>
</tr>
<tr>
<td><code>platform</code></td>
<td>
_(optional)_ The player's OS (`Android`, `Linux`, `Mac`, or `Windows`). This may be used to select updates.
</td>
</tr>
<tr>
<td><code>includeExtendedMetadata</code></td>
<td>
_(optional)_ Whether to include extra metadata that's not needed for SMAPI update checks, but which
may be useful to external tools.
</td>
</table>
Example request:
```js
POST https://api.smapi.io/v3.0/mods
{ {
"mods": [ "mods": [
{ {
"id": "Pathoschild.LookupAnything", "id": "Pathoschild.ContentPatcher",
"updateKeys": [ "nexus:541", "chucklefish:4250" ] "updateKeys": [ "nexus:1915" ],
"installedVersion": "1.9.2",
"isBroken": false
} }
], ],
"apiVersion": "3.0.0",
"gameVersion": "1.4.0",
"platform": "Windows",
"includeExtendedMetadata": true "includeExtendedMetadata": true
} }
``` ```
The API will automatically aggregate versions and errors. Each mod will include... Response fields:
* an `id` (matching what you passed in);
* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g.
optional files on Nexus), and `unofficial` if newer (from the wiki);
* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified
`includeExtendedMetadata: true`);
* and `errors` containing any error messages that occurred while fetching data.
For example: <table>
``` <tr>
<th>field</th>
<th>summary</th>
</tr>
<tr>
<td><code>id</code></td>
<td>
The mod ID you specified in the request.
</td>
</tr>
<tr>
<td><code>suggestedUpdate</code></td>
<td>
The update version recommended by the web API, if any. This is based on some internal rules (e.g.
it won't recommend a prerelease update if the player has a working stable version) and context
(e.g. whether the player is in the game beta channel). Choosing an update version yourself isn't
recommended, but you can set `includeExtendedMetadata: true` and check the `metadata` field if you
really want to do that.
</td>
</tr>
<tr>
<td><code>errors</code></td>
<td>
Human-readable errors that occurred fetching the version info (e.g. if a mod page has an invalid
version).
</td>
</tr>
<tr>
<td><code>metadata</code></td>
<td>
Extra metadata that's not needed for SMAPI update checks but which may be useful to external tools,
if you set `includeExtendedMetadata: true` in the request. Included fields:
field | summary
----- | -------
`id` | The known `manifest.json` unique IDs for this mod defined on the wiki, if any. That includes historical versions of the mod.
`name` | The normalised name for this mod based on the crossreferenced sites.
`nexusID` | The mod ID on [Nexus Mods](https://www.nexusmods.com/stardewvalley/), if any.
`chucklefishID` | The mod ID in the [Chucklefish mod repo](https://community.playstarbound.com/resources/categories/stardew-valley.22/), if any.
`curseForgeID` | The mod project ID on [CurseForge](https://www.curseforge.com/stardewvalley), if any.
`curseForgeKey` | The mod key on [CurseForge](https://www.curseforge.com/stardewvalley), if any. This is used in the mod page URL.
`modDropID` | The mod ID on [ModDrop](https://www.moddrop.com/stardew-valley), if any.
`gitHubRepo` | The GitHub repository containing the mod code, if any. Specified in the `Owner/Repo` form.
`customSourceUrl` | The custom URL to the mod code, if any. This is used for mods which aren't stored in a GitHub repo.
`customUrl` | The custom URL to the mod page, if any. This is used for mods which aren't stored on one of the standard mod sites covered by the ID fields.
`main` | The primary mod version, if any. This depends on the mod site, but it's typically either the version of the mod itself or of its latest non-optional download.
`optional` | The latest optional download version, if any.
`unofficial` | The version of the unofficial update defined on the wiki for this mod, if any.
`unofficialForBeta` | Equivalent to `unofficial`, but for beta versions of SMAPI or Stardew Valley.
`hasBetaInfo` | Whether there's an ongoing Stardew Valley or SMAPI beta which may affect update checks.
`compatibilityStatus` | The compatibility status for the mod for the stable version of the game, as defined on the wiki, if any. See [possible values](https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs).
`compatibilitySummary` | The human-readable summary of the mod's compatibility in HTML format, if any.
`brokeIn` | The SMAPI or Stardew Valley version that broke this mod, if any.
`betaCompatibilityStatus`<br />`betaCompatibilitySummary`<br />`betaBrokeIn` | Equivalent to the preceding fields, but for beta versions of SMAPI or Stardew Valley.
</td>
</tr>
</table>
Example response with `includeExtendedMetadata: false`:
```js
[ [
{ {
"id": "Pathoschild.LookupAnything", "id": "Pathoschild.ContentPatcher",
"main": { "suggestedUpdate": {
"version": "1.19", "version": "1.10.0",
"url": "https://www.nexusmods.com/stardewvalley/mods/541" "url": "https://www.nexusmods.com/stardewvalley/mods/1915"
},
"errors": []
}
]
```
Example response with `includeExtendedMetadata: true`:
```js
[
{
"id": "Pathoschild.ContentPatcher",
"suggestedUpdate": {
"version": "1.10.0",
"url": "https://www.nexusmods.com/stardewvalley/mods/1915"
}, },
"metadata": { "metadata": {
"id": [ "id": [ "Pathoschild.ContentPatcher" ],
"Pathoschild.LookupAnything", "name": "Content Patcher",
"LookupAnything" "nexusID": 1915,
], "curseForgeID": 309243,
"name": "Lookup Anything", "curseForgeKey": "content-patcher",
"nexusID": 541, "modDropID": 470174,
"gitHubRepo": "Pathoschild/StardewMods", "gitHubRepo": "Pathoschild/StardewMods",
"main": {
"version": "1.10",
"url": "https://www.nexusmods.com/stardewvalley/mods/1915"
},
"hasBetaInfo": true,
"compatibilityStatus": "Ok", "compatibilityStatus": "Ok",
"compatibilitySummary": "✓ use latest version." "compatibilitySummary": "✓ use latest version."
}, },

View File

@ -1,3 +1,5 @@
using System;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{ {
/// <summary>Metadata about a mod.</summary> /// <summary>Metadata about a mod.</summary>
@ -9,23 +11,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The mod's unique ID (if known).</summary> /// <summary>The mod's unique ID (if known).</summary>
public string ID { get; set; } public string ID { get; set; }
/// <summary>The main version.</summary> /// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary>
public ModEntryVersionModel Main { get; set; } public ModEntryVersionModel SuggestedUpdate { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
public ModEntryVersionModel UnofficialForBeta { get; set; }
/// <summary>Optional extended data which isn't needed for update checks.</summary> /// <summary>Optional extended data which isn't needed for update checks.</summary>
public ModExtendedMetadataModel Metadata { get; set; } public ModExtendedMetadataModel Metadata { get; set; }
/// <summary>The main version.</summary>
[Obsolete]
public ModEntryVersionModel Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
[Obsolete]
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
[Obsolete]
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
[Obsolete]
public ModEntryVersionModel UnofficialForBeta { get; set; }
/// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary> /// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary>
public bool HasBetaInfo { get; set; } [Obsolete]
public bool? HasBetaInfo { get; set; }
/// <summary>The errors that occurred while fetching update data.</summary> /// <summary>The errors that occurred while fetching update data.</summary>
public string[] Errors { get; set; } = new string[0]; public string[] Errors { get; set; } = new string[0];

View File

@ -46,6 +46,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The custom mod page URL (if applicable).</summary> /// <summary>The custom mod page URL (if applicable).</summary>
public string CustomUrl { get; set; } public string CustomUrl { get; set; }
/// <summary>The main version.</summary>
public ModEntryVersionModel Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
public ModEntryVersionModel UnofficialForBeta { get; set; }
/**** /****
** Stable compatibility ** Stable compatibility
@ -60,7 +71,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The game or SMAPI version which broke this mod, if applicable.</summary> /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
public string BrokeIn { get; set; } public string BrokeIn { get; set; }
/**** /****
** Beta compatibility ** Beta compatibility
****/ ****/
@ -84,8 +94,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="wiki">The mod metadata from the wiki (if available).</param> /// <param name="wiki">The mod metadata from the wiki (if available).</param>
/// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param> /// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param>
public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) /// <param name="main">The main version.</param>
/// <param name="optional">The latest optional version, if newer than <paramref name="main"/>.</param>
/// <param name="unofficial">The latest unofficial version, if newer than <paramref name="main"/> and <paramref name="optional"/>.</param>
/// <param name="unofficialForBeta">The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</param>
public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta)
{ {
// versions
this.Main = main;
this.Optional = optional;
this.Unofficial = unofficial;
this.UnofficialForBeta = unofficialForBeta;
// wiki data // wiki data
if (wiki != null) if (wiki != null)
{ {

View File

@ -12,6 +12,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The namespaced mod update keys (if available).</summary> /// <summary>The namespaced mod update keys (if available).</summary>
public string[] UpdateKeys { get; set; } public string[] UpdateKeys { get; set; }
/// <summary>The mod version installed by the local player. This is used for version mapping in some cases.</summary>
public ISemanticVersion InstalledVersion { get; set; }
/// <summary>Whether the installed version is broken or could not be loaded.</summary>
public bool IsBroken { get; set; }
/********* /*********
** Public methods ** Public methods
@ -24,10 +30,13 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="id">The unique mod ID.</param> /// <param name="id">The unique mod ID.</param>
/// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param>
/// <param name="updateKeys">The namespaced mod update keys (if available).</param> /// <param name="updateKeys">The namespaced mod update keys (if available).</param>
public ModSearchEntryModel(string id, string[] updateKeys) /// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param>
public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false)
{ {
this.ID = id; this.ID = id;
this.InstalledVersion = installedVersion;
this.UpdateKeys = updateKeys ?? new string[0]; this.UpdateKeys = updateKeys ?? new string[0];
} }
} }

View File

@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{ {
@ -14,6 +15,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Whether to include extended metadata for each mod.</summary> /// <summary>Whether to include extended metadata for each mod.</summary>
public bool IncludeExtendedMetadata { get; set; } public bool IncludeExtendedMetadata { get; set; }
/// <summary>The SMAPI version installed by the player. This is used for version mapping in some cases.</summary>
public ISemanticVersion ApiVersion { get; set; }
/// <summary>The Stardew Valley version installed by the player.</summary>
public ISemanticVersion GameVersion { get; set; }
/// <summary>The OS on which the player plays.</summary>
public Platform? Platform { get; set; }
/********* /*********
** Public methods ** Public methods
@ -26,10 +36,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="mods">The mods to search.</param> /// <param name="mods">The mods to search.</param>
/// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
/// <param name="gameVersion">The Stardew Valley version installed by the player.</param>
/// <param name="platform">The OS on which the player plays.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata)
{ {
this.Mods = mods.ToArray(); this.Mods = mods.ToArray();
this.ApiVersion = apiVersion;
this.GameVersion = gameVersion;
this.Platform = platform;
this.IncludeExtendedMetadata = includeExtendedMetadata; this.IncludeExtendedMetadata = includeExtendedMetadata;
} }
} }

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Net; using System.Net;
using Newtonsoft.Json; using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{ {
@ -37,12 +38,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Get metadata about a set of mods from the web API.</summary> /// <summary>Get metadata about a set of mods from the web API.</summary>
/// <param name="mods">The mod keys for which to fetch the latest version.</param> /// <param name="mods">The mod keys for which to fetch the latest version.</param>
/// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
/// <param name="gameVersion">The Stardew Valley version installed by the player.</param>
/// <param name="platform">The OS on which the player plays.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false)
{ {
return this.Post<ModSearchModel, ModEntryModel[]>( return this.Post<ModSearchModel, ModEntryModel[]>(
$"v{this.Version}/mods", $"v{this.Version}/mods",
new ModSearchModel(mods, includeExtendedMetadata) new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata)
).ToDictionary(p => p.ID); ).ToDictionary(p => p.ID);
} }

View File

@ -102,6 +102,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
string anchor = this.GetAttribute(node, "id"); string anchor = this.GetAttribute(node, "id");
string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for");
string devNote = this.GetAttribute(node, "data-dev-note"); string devNote = this.GetAttribute(node, "data-dev-note");
IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions");
IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions");
// parse stable compatibility // parse stable compatibility
WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
@ -159,6 +161,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
Warnings = warnings, Warnings = warnings,
MetadataLinks = metadataLinks.ToArray(), MetadataLinks = metadataLinks.ToArray(),
DevNote = devNote, DevNote = devNote,
MapLocalVersions = mapLocalVersions,
MapRemoteVersions = mapRemoteVersions,
Anchor = anchor Anchor = anchor
}; };
} }
@ -223,6 +227,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
return null; return null;
} }
/// <summary>Get an attribute value and parse it as a version mapping.</summary>
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
private IDictionary<string, string> GetAttributeAsVersionMapping(HtmlNode element, string name)
{
// get raw value
string raw = this.GetAttribute(element, name);
if (raw?.Contains("→") != true)
return null;
// parse
// Specified on the wiki in the form "remote version → mapped version; another remote version → mapped version"
IDictionary<string, string> map = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (string pair in raw.Split(';'))
{
string[] versions = pair.Split('→');
if (versions.Length == 2 && !string.IsNullOrWhiteSpace(versions[0]) && !string.IsNullOrWhiteSpace(versions[1]))
map[versions[0].Trim()] = versions[1].Trim();
}
return map;
}
/// <summary>Get the text of an element with the given class name.</summary> /// <summary>Get the text of an element with the given class name.</summary>
/// <param name="container">The metadata container.</param> /// <param name="container">The metadata container.</param>
/// <param name="className">The field name.</param> /// <param name="className">The field name.</param>

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{ {
@ -62,6 +63,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary> /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
public string DevNote { get; set; } public string DevNote { get; set; }
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapLocalVersions { get; set; }
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapRemoteVersions { get; set; }
/// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary> /// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
public string Anchor { get; set; } public string Anchor { get; set; }
} }

View File

@ -25,12 +25,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// </remarks> /// </remarks>
public string FormerIDs { get; set; } public string FormerIDs { get; set; }
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>();
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>();
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
public ModWarning SuppressWarnings { get; set; } public ModWarning SuppressWarnings { get; set; }

View File

@ -22,12 +22,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
public ModWarning SuppressWarnings { get; set; } public ModWarning SuppressWarnings { get; set; }
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapLocalVersions { get; }
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapRemoteVersions { get; }
/// <summary>The versioned field data.</summary> /// <summary>The versioned field data.</summary>
public ModDataField[] Fields { get; } public ModDataField[] Fields { get; }
@ -44,8 +38,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
this.ID = model.ID; this.ID = model.ID;
this.FormerIDs = model.GetFormerIDs().ToArray(); this.FormerIDs = model.GetFormerIDs().ToArray();
this.SuppressWarnings = model.SuppressWarnings; this.SuppressWarnings = model.SuppressWarnings;
this.MapLocalVersions = new Dictionary<string, string>(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase);
this.MapRemoteVersions = new Dictionary<string, string>(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase);
this.Fields = model.GetFields().ToArray(); this.Fields = model.GetFields().ToArray();
} }
@ -67,29 +59,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
return false; return false;
} }
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The remote version to normalize.</param>
public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
{
return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion)
? new SemanticVersion(newVersion)
: version;
}
/// <summary>Get a semantic remote version for update checks.</summary>
/// <param name="version">The remote version to normalize.</param>
public string GetRemoteVersionForUpdateChecks(string version)
{
// normalize version if possible
if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
version = parsed.ToString();
// fetch remote version
return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion)
? newVersion
: version;
}
/// <summary>Get the possible mod IDs.</summary> /// <summary>Get the possible mod IDs.</summary>
public IEnumerable<string> GetIDs() public IEnumerable<string> GetIDs()
{ {

View File

@ -26,29 +26,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary>
public ISemanticVersion StatusUpperVersion { get; set; } public ISemanticVersion StatusUpperVersion { get; set; }
/*********
** Public methods
*********/
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The remote version to normalize.</param>
public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
{
return this.DataRecord.GetLocalVersionForUpdateChecks(version);
}
/// <summary>Get a semantic remote version for update checks.</summary>
/// <param name="version">The remote version to normalize.</param>
public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version)
{
if (version == null)
return null;
string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString());
return rawVersion != null
? new SemanticVersion(rawVersion)
: version;
}
} }
} }

View File

@ -80,7 +80,7 @@ namespace StardewModdingAPI.Web.Controllers
new IModRepository[] new IModRepository[]
{ {
new ChucklefishRepository(chucklefish), new ChucklefishRepository(chucklefish),
new CurseForgeRepository(curseForge), new CurseForgeRepository(curseForge),
new GitHubRepository(github), new GitHubRepository(github),
new ModDropRepository(modDrop), new ModDropRepository(modDrop),
new NexusRepository(nexus) new NexusRepository(nexus)
@ -90,12 +90,15 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Fetch version metadata for the given mods.</summary> /// <summary>Fetch version metadata for the given mods.</summary>
/// <param name="model">The mod search criteria.</param> /// <param name="model">The mod search criteria.</param>
/// <param name="version">The requested API version.</param>
[HttpPost] [HttpPost]
public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model) public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version)
{ {
if (model?.Mods == null) if (model?.Mods == null)
return new ModEntryModel[0]; return new ModEntryModel[0];
bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109");
// fetch wiki data // fetch wiki data
WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray();
IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase); IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
@ -104,7 +107,25 @@ namespace StardewModdingAPI.Web.Controllers
if (string.IsNullOrWhiteSpace(mod.ID)) if (string.IsNullOrWhiteSpace(mod.ID))
continue; continue;
ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion);
if (legacyMode)
{
result.Main = result.Metadata.Main;
result.Optional = result.Metadata.Optional;
result.Unofficial = result.Metadata.Unofficial;
result.UnofficialForBeta = result.Metadata.UnofficialForBeta;
result.HasBetaInfo = result.Metadata.BetaCompatibilityStatus != null;
result.SuggestedUpdate = null;
if (!model.IncludeExtendedMetadata)
result.Metadata = null;
}
else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null))
{
var errors = new List<string>(result.Errors);
errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage.");
result.Errors = errors.ToArray();
}
mods[mod.ID] = result; mods[mod.ID] = result;
} }
@ -120,8 +141,9 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="search">The mod data to match.</param> /// <param name="search">The mod data to match.</param>
/// <param name="wikiData">The wiki data.</param> /// <param name="wikiData">The wiki data.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
/// <param name="apiVersion">The SMAPI version installed by the player.</param>
/// <returns>Returns the mod data if found, else <c>null</c>.</returns> /// <returns>Returns the mod data if found, else <c>null</c>.</returns>
private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion)
{ {
// cross-reference data // cross-reference data
ModDataRecord record = this.ModDatabase.Get(search.ID); ModDataRecord record = this.ModDatabase.Get(search.ID);
@ -131,6 +153,10 @@ namespace StardewModdingAPI.Web.Controllers
// get latest versions // get latest versions
ModEntryModel result = new ModEntryModel { ID = search.ID }; ModEntryModel result = new ModEntryModel { ID = search.ID };
IList<string> errors = new List<string>(); IList<string> errors = new List<string>();
ModEntryVersionModel main = null;
ModEntryVersionModel optional = null;
ModEntryVersionModel unofficial = null;
ModEntryVersionModel unofficialForBeta = null;
foreach (UpdateKey updateKey in updateKeys) foreach (UpdateKey updateKey in updateKeys)
{ {
// validate update key // validate update key
@ -151,76 +177,118 @@ namespace StardewModdingAPI.Web.Controllers
// handle main version // handle main version
if (data.Version != null) if (data.Version != null)
{ {
if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions);
if (version == null)
{ {
errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
continue; continue;
} }
if (this.IsNewer(version, result.Main?.Version)) if (this.IsNewer(version, main?.Version))
result.Main = new ModEntryVersionModel(version, data.Url); main = new ModEntryVersionModel(version, data.Url);
} }
// handle optional version // handle optional version
if (data.PreviewVersion != null) if (data.PreviewVersion != null)
{ {
if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions);
if (version == null)
{ {
errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
continue; continue;
} }
if (this.IsNewer(version, result.Optional?.Version)) if (this.IsNewer(version, optional?.Version))
result.Optional = new ModEntryVersionModel(version, data.Url); optional = new ModEntryVersionModel(version, data.Url);
} }
} }
// get unofficial version // get unofficial version
if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version))
result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}");
// get unofficial version for beta // get unofficial version for beta
if (wikiEntry?.HasBetaInfo == true) if (wikiEntry?.HasBetaInfo == true)
{ {
result.HasBetaInfo = true;
if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial)
{ {
if (wikiEntry.BetaCompatibility.UnofficialVersion != null) if (wikiEntry.BetaCompatibility.UnofficialVersion != null)
{ {
result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version))
? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}")
: null; : null;
} }
else else
result.UnofficialForBeta = result.Unofficial; unofficialForBeta = unofficial;
} }
} }
// fallback to preview if latest is invalid // fallback to preview if latest is invalid
if (result.Main == null && result.Optional != null) if (main == null && optional != null)
{ {
result.Main = result.Optional; main = optional;
result.Optional = null; optional = null;
} }
// special cases // special cases
if (result.ID == "Pathoschild.SMAPI") if (result.ID == "Pathoschild.SMAPI")
{ {
if (result.Main != null) if (main != null)
result.Main.Url = "https://smapi.io/"; main.Url = "https://smapi.io/";
if (result.Optional != null) if (optional != null)
result.Optional.Url = "https://smapi.io/"; optional.Url = "https://smapi.io/";
}
// get recommended update (if any)
ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions);
if (apiVersion != null && installedVersion != null)
{
// get newer versions
List<ModEntryVersionModel> updates = new List<ModEntryVersionModel>();
if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true))
updates.Add(main);
if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease()))
updates.Add(optional);
if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken))
updates.Add(unofficial);
if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease()))
updates.Add(unofficialForBeta);
// get newest version
ModEntryVersionModel newest = null;
foreach (ModEntryVersionModel update in updates)
{
if (newest == null || update.Version.IsNewerThan(newest.Version))
newest = update;
}
// set field
result.SuggestedUpdate = newest != null
? new ModEntryVersionModel(newest.Version, newest.Url)
: null;
} }
// add extended metadata // add extended metadata
if (includeExtendedMetadata && (wikiEntry != null || record != null)) if (includeExtendedMetadata)
result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta);
// add result // add result
result.Errors = errors.ToArray(); result.Errors = errors.ToArray();
return result; return result;
} }
/// <summary>Get whether a given version should be offered to the user as an update.</summary>
/// <param name="currentVersion">The current semantic version.</param>
/// <param name="newVersion">The target semantic version.</param>
/// <param name="useBetaChannel">Whether the user enabled the beta channel and should be offered prerelease updates.</param>
private bool IsRecommendedUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel)
{
return
newVersion != null
&& newVersion.IsNewerThan(currentVersion)
&& (useBetaChannel || !newVersion.IsPrerelease());
}
/// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary> /// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary>
/// <param name="current">The current version.</param> /// <param name="current">The current version.</param>
/// <param name="other">The other version.</param> /// <param name="other">The other version.</param>
@ -260,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="specifiedKeys">The specified update keys.</param> /// <param name="specifiedKeys">The specified update keys.</param>
/// <param name="record">The mod's entry in SMAPI's internal database.</param> /// <param name="record">The mod's entry in SMAPI's internal database.</param>
/// <param name="entry">The mod's entry in the wiki list.</param> /// <param name="entry">The mod's entry in the wiki list.</param>
public IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
{ {
IEnumerable<string> GetRaw() IEnumerable<string> GetRaw()
{ {
@ -301,5 +369,49 @@ namespace StardewModdingAPI.Web.Controllers
yield return key; yield return key;
} }
} }
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to parse.</param>
/// <param name="map">A map of version replacements.</param>
private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map)
{
// try mapped version
string rawNewVersion = this.GetRawMappedVersion(version, map);
if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew))
return parsedNew;
// return original version
return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld)
? parsedOld
: null;
}
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to map.</param>
/// <param name="map">A map of version replacements.</param>
private string GetRawMappedVersion(string version, IDictionary<string, string> map)
{
if (version == null || map == null || !map.Any())
return version;
// match exact raw version
if (map.ContainsKey(version))
return map[version];
// match parsed version
if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
{
if (map.ContainsKey(parsed.ToString()))
return map[parsed.ToString()];
foreach (var pair in map)
{
if (SemanticVersion.TryParse(pair.Key, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, out ISemanticVersion newVersion))
return newVersion.ToString();
}
}
return version;
}
} }
} }

View File

@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Options;
using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
@ -109,6 +112,17 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
/// <summary>The URL to the latest unofficial update, if applicable.</summary> /// <summary>The URL to the latest unofficial update, if applicable.</summary>
public string BetaUnofficialUrl { get; set; } public string BetaUnofficialUrl { get; set; }
/****
** Version maps
****/
/// <summary>Maps local versions to a semantic version for update checks.</summary>
[BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)]
public IDictionary<string, string> MapLocalVersions { get; set; }
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
[BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)]
public IDictionary<string, string> MapRemoteVersions { get; set; }
/********* /*********
** Accessors ** Accessors
@ -154,6 +168,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn; this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn;
this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString(); this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString();
this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl; this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl;
// version maps
this.MapLocalVersions = mod.MapLocalVersions;
this.MapRemoteVersions = mod.MapRemoteVersions;
} }
/// <summary>Reconstruct the original model.</summary> /// <summary>Reconstruct the original model.</summary>
@ -186,7 +204,11 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
BrokeIn = this.MainBrokeIn, BrokeIn = this.MainBrokeIn,
UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null, UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null,
UnofficialUrl = this.MainUnofficialUrl UnofficialUrl = this.MainUnofficialUrl
} },
// version maps
MapLocalVersions = this.MapLocalVersions,
MapRemoteVersions = this.MapRemoteVersions
}; };
// beta compatibility // beta compatibility

View File

@ -24,7 +24,7 @@
<p>This website and SMAPI's web API are hosted by Amazon Web Services. Their servers may automatically collect diagnostics like your IP address, but this information is not visible to SMAPI's web application or developers. For more information, see the <a href="https://aws.amazon.com/privacy/">Amazon Privacy Notice</a>.</p> <p>This website and SMAPI's web API are hosted by Amazon Web Services. Their servers may automatically collect diagnostics like your IP address, but this information is not visible to SMAPI's web application or developers. For more information, see the <a href="https://aws.amazon.com/privacy/">Amazon Privacy Notice</a>.</p>
<h3>Update checks</h3> <h3>Update checks</h3>
<p>SMAPI notifies you when there's a new version of SMAPI or your mods available. To do so, it sends your SMAPI and mod versions to its web API. No personal information is stored by the web application, but see <em><a href="#web-logging">web logging</a></em>.</p> <p>SMAPI notifies you when there's a new version of SMAPI or your mods available. To do so, it sends your game/SMAPI/mod versions and platform type to its web API. No personal information is stored by the web application, but see <em><a href="#web-logging">web logging</a></em>.</p>
<p>You can disable update checks, and no information will be transmitted to the web API. To do so:</p> <p>You can disable update checks, and no information will be transmitted to the web API. To do so:</p>
<ol> <ol>

View File

@ -14,11 +14,6 @@
* other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple
* variants can be separated with '|'. * variants can be separated with '|'.
* *
* - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions
* during update checks. For example, if the API returns version '1.1-1078' where '1078' is
* intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the
* mod's current version. This is only meant to support legacy mods with injected update keys.
*
* Versioned metadata * Versioned metadata
* ================== * ==================
* Each record can also specify extra metadata using the field keys below. * Each record can also specify extra metadata using the field keys below.
@ -122,91 +117,6 @@
"Default | UpdateKey": "Nexus:1820" "Default | UpdateKey": "Nexus:1820"
}, },
/*********
** Map versions
*********/
"Adjust Artisan Prices": {
"ID": "ThatNorthernMonkey.AdjustArtisanPrices",
"FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update
"MapRemoteVersions": { "0.01": "0.0.1" }
},
"Almighty Farming Tool": {
"ID": "439",
"MapRemoteVersions": {
"1.21": "1.2.1",
"1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion"
}
},
"Basic Sprinkler Improved": {
"ID": "lrsk_sdvm_bsi.0117171308",
"MapRemoteVersions": { "1.0.2": "1.0.1-release" } // manifest not updated
},
"Better Shipping Box": {
"ID": "Kithio:BetterShippingBox",
"MapLocalVersions": { "1.0.1": "1.0.2" }
},
"Chefs Closet": {
"ID": "Duder.ChefsCloset",
"MapLocalVersions": { "1.3-1": "1.3" }
},
"Configurable Machines": {
"ID": "21da6619-dc03-4660-9794-8e5b498f5b97",
"MapLocalVersions": { "1.2-beta": "1.2" }
},
"Crafting Counter": {
"ID": "lolpcgaming.CraftingCounter",
"MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest
},
"Custom Linens": {
"ID": "Mevima.CustomLinens",
"MapRemoteVersions": { "1.1": "1.0" } // manifest not updated
},
"Dynamic Horses": {
"ID": "Bpendragon-DynamicHorses",
"MapRemoteVersions": { "1.2": "1.1-release" } // manifest not updated
},
"Dynamic Machines": {
"ID": "DynamicMachines",
"MapLocalVersions": { "1.1": "1.1.1" }
},
"Multiple Sprites and Portraits On Rotation (File Loading)": {
"ID": "FileLoading",
"MapLocalVersions": { "1.1": "1.12" }
},
"Relationship Status": {
"ID": "relationshipstatus",
"MapRemoteVersions": { "1.0.5": "1.0.4" } // not updated in manifest
},
"ReRegeneration": {
"ID": "lrsk_sdvm_rerg.0925160827",
"MapLocalVersions": { "1.1.2-release": "1.1.2" }
},
"Showcase Mod": {
"ID": "Igorious.Showcase",
"MapLocalVersions": { "0.9-500": "0.9" }
},
"Siv's Marriage Mod": {
"ID": "6266959802", // official version
"FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions
"MapLocalVersions": { "0.0": "1.4" }
},
/********* /*********
** Obsolete ** Obsolete
*********/ *********/
@ -477,12 +387,6 @@
"~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors) "~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors)
}, },
"Skill Prestige: Cooking Adapter": {
"ID": "Alphablackwolf.CookingSkillPrestigeAdapter",
"FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1
"MapRemoteVersions": { "1.2.3": "1.1" } // manifest not updated
},
"Skull Cave Saver": { "Skull Cave Saver": {
"ID": "cantorsdust.SkullCaveSaver", "ID": "cantorsdust.SkullCaveSaver",
"FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2
@ -501,7 +405,6 @@
"Stephan's Lots of Crops": { "Stephan's Lots of Crops": {
"ID": "stephansstardewcrops", "ID": "stephansstardewcrops",
"MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated
"~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items) "~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items)
}, },

View File

@ -593,27 +593,19 @@ namespace StardewModdingAPI.Framework
ISemanticVersion updateFound = null; ISemanticVersion updateFound = null;
try try
{ {
ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; // fetch update check
ISemanticVersion latestStable = response.Main?.Version; ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value;
ISemanticVersion latestBeta = response.Optional?.Version; if (response.SuggestedUpdate != null)
this.Monitor.Log($"You can update SMAPI to {response.SuggestedUpdate.Version}: {Constants.HomePageUrl}", LogLevel.Alert);
else
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
if (latestStable == null && response.Errors.Any()) // show errors
if (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("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: {string.Join("\n", response.Errors)}", LogLevel.Trace); this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}", LogLevel.Trace);
} }
else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel))
{
updateFound = latestBeta;
this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert);
}
else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel))
{
updateFound = latestStable;
this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert);
}
else
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -646,12 +638,12 @@ namespace StardewModdingAPI.Framework
.GetUpdateKeys(validOnly: true) .GetUpdateKeys(validOnly: true)
.Select(p => p.ToString()) .Select(p => p.ToString())
.ToArray(); .ToArray();
searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.ToArray())); searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, mod.Manifest.Version, updateKeys.ToArray(), isBroken: mod.Status == ModMetadataStatus.Failed));
} }
// fetch results // fetch results
this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace);
IDictionary<string, ModEntryModel> results = client.GetModInfo(searchMods.ToArray()); IDictionary<string, ModEntryModel> results = client.GetModInfo(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform);
// extract update alerts & errors // extract update alerts & errors
var updates = new List<Tuple<IModMetadata, ISemanticVersion, string>>(); var updates = new List<Tuple<IModMetadata, ISemanticVersion, string>>();
@ -672,20 +664,9 @@ namespace StardewModdingAPI.Framework
); );
} }
// parse versions // handle update
bool useBetaInfo = result.HasBetaInfo && Constants.ApiVersion.IsPrerelease(); if (result.SuggestedUpdate != null)
ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; updates.Add(Tuple.Create(mod, result.SuggestedUpdate.Version, result.SuggestedUpdate.Url));
ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version;
ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version;
ISemanticVersion unofficialVersion = useBetaInfo ? result.UnofficialForBeta?.Version : result.Unofficial?.Version;
// show update alerts
if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true))
updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url));
else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease()))
updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url));
else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed))
updates.Add(Tuple.Create(mod, unofficialVersion, useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url));
} }
// show update errors // show update errors
@ -720,18 +701,6 @@ namespace StardewModdingAPI.Framework
}).Start(); }).Start();
} }
/// <summary>Get whether a given version should be offered to the user as an update.</summary>
/// <param name="currentVersion">The current semantic version.</param>
/// <param name="newVersion">The target semantic version.</param>
/// <param name="useBetaChannel">Whether the user enabled the beta channel and should be offered prerelease updates.</param>
private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel)
{
return
newVersion != null
&& newVersion.IsNewerThan(currentVersion)
&& (useBetaChannel || !newVersion.IsPrerelease());
}
/// <summary>Create a directory path if it doesn't exist.</summary> /// <summary>Create a directory path if it doesn't exist.</summary>
/// <param name="path">The directory path.</param> /// <param name="path">The directory path.</param>
private void VerifyPath(string path) private void VerifyPath(string path)