summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/assets/install on Linux.sh2
-rw-r--r--src/SMAPI.Installer/assets/install on Windows.bat4
-rw-r--r--src/SMAPI.Installer/assets/install on macOS.command2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj4
-rw-r--r--src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Tests/SMAPI.Tests.csproj10
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs5
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs42
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj6
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs23
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModDownload.cs18
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModPage.cs17
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs7
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs35
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs30
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs28
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs106
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs34
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs71
-rw-r--r--src/SMAPI.Web/Framework/IModDownload.cs11
-rw-r--r--src/SMAPI.Web/Framework/IModPage.cs11
-rw-r--r--src/SMAPI.Web/Framework/ModInfoModel.cs30
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs120
-rw-r--r--src/SMAPI.Web/Framework/RemoteModStatus.cs3
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj16
-rw-r--r--src/SMAPI.Web/Startup.cs3
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json4
-rw-r--r--src/SMAPI.sln5
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Context.cs2
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs13
-rw-r--r--src/SMAPI/Framework/SCore.cs12
-rw-r--r--src/SMAPI/Framework/SModHooks.cs18
-rw-r--r--src/SMAPI/SMAPI.csproj8
-rw-r--r--src/SMAPI/Utilities/DelegatingModHooks.cs137
-rw-r--r--src/SMAPI/Utilities/PerScreen.cs6
-rw-r--r--src/SMAPI/i18n/ko.json4
41 files changed, 733 insertions, 142 deletions
diff --git a/src/SMAPI.Installer/assets/install on Linux.sh b/src/SMAPI.Installer/assets/install on Linux.sh
index 3b7eae9c..70b21521 100644
--- a/src/SMAPI.Installer/assets/install on Linux.sh
+++ b/src/SMAPI.Installer/assets/install on Linux.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
cd "`dirname "$0"`"
internal/linux/SMAPI.Installer
diff --git a/src/SMAPI.Installer/assets/install on Windows.bat b/src/SMAPI.Installer/assets/install on Windows.bat
index b0d9ae81..c61a801e 100644
--- a/src/SMAPI.Installer/assets/install on Windows.bat
+++ b/src/SMAPI.Installer/assets/install on Windows.bat
@@ -4,7 +4,9 @@ setlocal enabledelayedexpansion
SET installerDir="%~dp0"
REM make sure we're not running within a zip folder
-echo %installerDir% | findstr /C:"%TEMP%" 1>nul
+REM The error level is usually 0 (install dir contains temp path), 1 (it doesn't), or 9009 (findstr doesn't exist due to a Windows issue).
+REM If the command doesn't exist, just skip this check.
+echo %installerDir% | findstr /C:"%TEMP%" 1>nul 2>null
if %ERRORLEVEL% EQU 0 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
echo.
diff --git a/src/SMAPI.Installer/assets/install on macOS.command b/src/SMAPI.Installer/assets/install on macOS.command
index abd21dc8..e85230ed 100644
--- a/src/SMAPI.Installer/assets/install on macOS.command
+++ b/src/SMAPI.Installer/assets/install on macOS.command
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
cd "`dirname "$0"`"
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
index 3be9c225..1719d39b 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
@@ -6,9 +6,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
- <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
+ <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
</ItemGroup>
<ItemGroup>
diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
index cded6f65..badabfc7 100644
--- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
+++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
@@ -10,7 +10,7 @@
<!--NuGet package-->
<PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId>
<Title>Build package for SMAPI mods</Title>
- <Version>4.0.2</Version>
+ <Version>4.1.0</Version>
<Authors>Pathoschild</Authors>
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@@ -24,7 +24,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" />
- <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<!--
This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 0afb5837..2447c5c3 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.18.1",
+ "Version": "3.18.3",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.18.1"
+ "MinimumApiVersion": "3.18.3"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index fe802d88..306c92fc 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.18.1",
+ "Version": "3.18.3",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.18.1"
+ "MinimumApiVersion": "3.18.3"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 9a587a2b..c5075c57 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.18.1",
+ "Version": "3.18.3",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.18.1"
+ "MinimumApiVersion": "3.18.3"
}
diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj
index 597cd7dd..0b1fb638 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -14,12 +14,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="FluentAssertions" Version="6.7.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
- <PackageReference Include="Moq" Version="4.18.1" />
- <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
+ <PackageReference Include="FluentAssertions" Version="6.8.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
- <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
+ <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
</ItemGroup>
<ItemGroup>
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs
index 47cd3f7e..195b0367 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs
@@ -19,6 +19,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
ModDrop,
/// <summary>The Nexus Mods mod repository.</summary>
- Nexus
+ Nexus,
+
+ /// <summary>An arbitrary URL to a JSON file containing update data.</summary>
+ UpdateManifest
}
}
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
index 960caf96..3e8064fd 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
@@ -58,31 +58,17 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <param name="raw">The raw update key to parse.</param>
public static UpdateKey Parse(string? raw)
{
+ if (raw is null)
+ return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
// extract site + ID
- string? rawSite;
- string? id;
- {
- string[]? parts = raw?.Trim().Split(':');
- if (parts?.Length != 2)
- return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
-
- rawSite = parts[0].Trim();
- id = parts[1].Trim();
- }
- if (string.IsNullOrWhiteSpace(id))
+ (string rawSite, string? id) = UpdateKey.SplitTwoParts(raw, ':');
+ if (string.IsNullOrEmpty(id))
id = null;
// extract subkey
string? subkey = null;
if (id != null)
- {
- string[] parts = id.Split('@');
- if (parts.Length == 2)
- {
- id = parts[0].Trim();
- subkey = $"@{parts[1]}".Trim();
- }
- }
+ (id, subkey) = UpdateKey.SplitTwoParts(id, '@', true);
// parse
if (!Enum.TryParse(rawSite, true, out ModSiteKey site))
@@ -151,5 +137,23 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
{
return $"{site}:{id}{subkey}".Trim();
}
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Split a string into two parts at a delimiter and trim whitespace.</summary>
+ /// <param name="str">The string to split.</param>
+ /// <param name="delimiter">The character on which to split.</param>
+ /// <param name="keepDelimiter">Whether to include the delimiter in the second string.</param>
+ /// <returns>Returns a tuple containing the two strings, with the second value <c>null</c> if the delimiter wasn't found.</returns>
+ private static (string, string?) SplitTwoParts(string str, char delimiter, bool keepDelimiter = false)
+ {
+ int splitIndex = str.IndexOf(delimiter);
+
+ return splitIndex >= 0
+ ? (str.Substring(0, splitIndex).Trim(), str.Substring(splitIndex + (keepDelimiter ? 0 : 1)).Trim())
+ : (str.Trim(), null);
+ }
}
}
diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
index 10f1df70..2a9a8294 100644
--- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -9,9 +9,9 @@
<Import Project="..\..\build\common.targets" />
<ItemGroup>
- <PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
- <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
+ <PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.3.0" />
<PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="VdfConverter" Version="1.0.3" Condition="'$(OS)' == 'Windows_NT'" Private="False" />
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 71fb42c2..f687c7dd 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -22,6 +22,7 @@ using StardewModdingAPI.Web.Framework.Clients.CurseForge;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
+using StardewModdingAPI.Web.Framework.Clients.UpdateManifest;
using StardewModdingAPI.Web.Framework.ConfigModels;
namespace StardewModdingAPI.Web.Controllers
@@ -63,14 +64,15 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="github">The GitHub API client.</param>
/// <param name="modDrop">The ModDrop API client.</param>
/// <param name="nexus">The Nexus API client.</param>
- public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
+ /// <param name="updateManifest">The API client for arbitrary update manifest URLs.</param>
+ public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest)
{
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
this.WikiCache = wikiCache;
this.ModCache = modCache;
this.Config = config;
- this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus });
+ this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus, updateManifest });
}
/// <summary>Fetch version metadata for the given mods.</summary>
@@ -145,7 +147,12 @@ namespace StardewModdingAPI.Web.Controllers
foreach (UpdateKey updateKey in updateKeys)
{
// validate update key
- if (!updateKey.LooksValid)
+ if (
+ !updateKey.LooksValid
+#if SMAPI_DEPRECATED
+ || (updateKey.Site == ModSiteKey.UpdateManifest && apiVersion?.IsNewerThan("4.0.0-alpha") != true) // 4.0-alpha feature, don't make available to released mods in case it changes before release
+#endif
+ )
{
errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'.");
continue;
@@ -162,17 +169,21 @@ namespace StardewModdingAPI.Web.Controllers
// if there's only a prerelease version (e.g. from GitHub), don't override the main version
ISemanticVersion? curMain = data.Version;
ISemanticVersion? curPreview = data.PreviewVersion;
+ string? curMainUrl = data.MainModPageUrl;
+ string? curPreviewUrl = data.PreviewModPageUrl;
if (curPreview == null && curMain?.IsPrerelease() == true)
{
curPreview = curMain;
+ curPreviewUrl = curMainUrl;
curMain = null;
+ curMainUrl = null;
}
// handle versions
if (this.IsNewer(curMain, main?.Version))
- main = new ModEntryVersionModel(curMain, data.Url!);
+ main = new ModEntryVersionModel(curMain, curMainUrl ?? data.Url!);
if (this.IsNewer(curPreview, optional?.Version))
- optional = new ModEntryVersionModel(curPreview, data.Url!);
+ optional = new ModEntryVersionModel(curPreview, curPreviewUrl ?? data.Url!);
}
// get unofficial version
@@ -295,7 +306,7 @@ namespace StardewModdingAPI.Web.Controllers
}
// get version info
- return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions);
+ return this.ModSites.GetPageVersions(page, updateKey, allowNonStandardVersions, mapRemoteVersions);
}
/// <summary>Get update keys based on the available mod metadata, while maintaining the precedence order.</summary>
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
index 548f17c3..6c9c08ef 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace StardewModdingAPI.Web.Framework.Clients
{
/// <summary>Generic metadata about a file download on a mod page.</summary>
@@ -15,6 +17,9 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <summary>The download's file version.</summary>
public string? Version { get; }
+ /// <summary>The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</summary>
+ public string? ModPageUrl { get; }
+
/*********
** Public methods
@@ -23,11 +28,22 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <param name="name">The download's display name.</param>
/// <param name="description">The download's description.</param>
/// <param name="version">The download's file version.</param>
- public GenericModDownload(string name, string? description, string? version)
+ /// <param name="modPageUrl">The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</param>
+ public GenericModDownload(string name, string? description, string? version, string? modPageUrl = null)
{
this.Name = name;
this.Description = description;
this.Version = version;
+ this.ModPageUrl = modPageUrl;
+ }
+
+ /// <summary>Get whether the subkey matches this download.</summary>
+ /// <param name="subkey">The update subkey to check.</param>
+ public virtual bool MatchesSubkey(string subkey)
+ {
+ return
+ this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase)
+ || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true;
}
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
index 5353c7e1..63ca5a95 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
@@ -40,6 +40,9 @@ namespace StardewModdingAPI.Web.Framework.Clients
[MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
public bool IsValid => this.Status == RemoteModStatus.Ok;
+ /// <summary>Whether this mod page requires update subkeys and does not allow matching downloads without them.</summary>
+ public bool RequireSubkey { get; set; } = false;
+
/*********
** Public methods
@@ -79,5 +82,19 @@ namespace StardewModdingAPI.Web.Framework.Clients
return this;
}
+
+ /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary>
+ /// <param name="subkey">The update subkey.</param>
+ public virtual string? GetName(string? subkey)
+ {
+ return this.Name;
+ }
+
+ /// <summary>Get the mod page URL for an update subkey, if different from the mod page it was fetched from.</summary>
+ /// <param name="subkey">The update subkey.</param>
+ public virtual string? GetUrl(string? subkey)
+ {
+ return this.Url;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs
new file mode 100644
index 00000000..bf1edd3f
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs
@@ -0,0 +1,7 @@
+using System;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary>
+ internal interface IUpdateManifestClient : IModSiteClient, IDisposable { }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs
new file mode 100644
index 00000000..ead5c229
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels
+{
+ /// <summary>The data model for a mod in an update manifest file.</summary>
+ internal class UpdateManifestModModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's name.</summary>
+ public string? Name { get; }
+
+ /// <summary>The mod page URL from which to download updates.</summary>
+ public string? ModPageUrl { get; }
+
+ /// <summary>The available versions for this mod.</summary>
+ public UpdateManifestVersionModel[] Versions { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod's name.</param>
+ /// <param name="modPageUrl">The mod page URL from which to download updates.</param>
+ /// <param name="versions">The available versions for this mod.</param>
+ public UpdateManifestModModel(string? name, string? modPageUrl, UpdateManifestVersionModel[]? versions)
+ {
+ this.Name = name;
+ this.ModPageUrl = modPageUrl;
+ this.Versions = versions ?? Array.Empty<UpdateManifestVersionModel>();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs
new file mode 100644
index 00000000..5ccd31b0
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels
+{
+ /// <summary>The data model for an update manifest file.</summary>
+ internal class UpdateManifestModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The manifest format version. This is equivalent to the SMAPI version, and is used to parse older manifests correctly if later versions of SMAPI change the expected format.</summary>
+ public string Format { get; }
+
+ /// <summary>The mod info in this update manifest.</summary>
+ public IDictionary<string, UpdateManifestModModel> Mods { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="format">The manifest format version.</param>
+ /// <param name="mods">The mod info in this update manifest.</param>
+ public UpdateManifestModel(string format, IDictionary<string, UpdateManifestModModel>? mods)
+ {
+ this.Format = format;
+ this.Mods = mods ?? new Dictionary<string, UpdateManifestModModel>();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs
new file mode 100644
index 00000000..6678f5eb
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs
@@ -0,0 +1,28 @@
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels
+{
+ /// <summary>Data model for a Version in an update manifest.</summary>
+ internal class UpdateManifestVersionModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's semantic version.</summary>
+ public string? Version { get; }
+
+ /// <summary>The mod page URL from which to download updates, if different from <see cref="UpdateManifestModModel.ModPag