summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md30
-rw-r--r--.github/ISSUE_TEMPLATE/config.yml8
-rw-r--r--.github/ISSUE_TEMPLATE/custom.md37
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md12
-rw-r--r--.github/ISSUE_TEMPLATE/general.md12
-rw-r--r--build/common.targets2
-rwxr-xr-xbuild/unix/prepare-install-package.sh2
-rwxr-xr-xbuild/unix/set-smapi-version.sh2
-rwxr-xr-xbuild/windows/finalize-install-package.sh2
-rw-r--r--docs/release-notes.md32
-rw-r--r--docs/technical/mod-package.md10
-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
52 files changed, 821 insertions, 203 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index c51d164b..00000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-name: Bug report
-about: Report a problem with SMAPI.
-
----
-
-<!--
-
-Only report a bug here if you're sure it's a SMAPI bug!
-To request support instead, see https://smapi.io/community.
-
-Replace the instructions below with the bug details.
-
--->
-
-**Describe the bug**
-A clear and concise description of what the bug is. Provide any other details you think might be relevant here.
-
-**To Reproduce**
-Exact steps which reproduce the bug, if possible. For example:
-1. Load save '...'.
-2. Walk to '....'.
-3. Click '....'.
-4. Error occurs.
-
-**Log file**
-Upload your SMAPI log to https://smapi.io/log and post a link here.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..63d7bb67
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Troubleshooting guide for players
+ url: https://smapi.io/troubleshoot
+ about: See if your question is already answered first!
+ - name: Get help or discuss
+ url: https://smapi.io/help
+ about: Ask for help from the community, or join the Stardew Valley Discord to ask questions, report issues, or discuss with the SMAPI developer, players, and mod authors. The SMAPI developer is @Pathoschild#0001 on Discord.
diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md
new file mode 100644
index 00000000..11306e7f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/custom.md
@@ -0,0 +1,37 @@
+---
+name: Create a development task
+about: DON'T DO THIS BEFORE READING. This is for specific changes to the code or technical bug reports. See below if something isn't working, you have questions or ideas, or you want to discuss something.
+
+---
+
+<!--
+
+
+
+
+STOP!
+
+Is this a specific development task? Don't create an issue if not!
+See https://smapi.io/community if something isn't working, you have questions or ideas, or you want
+to discuss something.
+
+If you're absolutely sure it's a specific development task (e.g. a specific bug, not just
+'something went wrong on my computer'), edit the template below.
+
+-->
+
+**Describe the bug**
+A clear and concise description of what the bug is. Provide any other details you think might be relevant here.
+
+**To Reproduce**
+Exact steps which reproduce the bug, if possible. For example:
+1. Load save '...'.
+2. Walk to '....'.
+3. Click '....'.
+4. Error occurs.
+
+**Log file**
+Upload your SMAPI log to https://smapi.io/log and post a link here.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 8d935dc8..00000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for SMAPI.
-
----
-
-<!--
-
-GitHub issues are only used for development tasks. Please don't submit feature requests here!
-Instead, see https://smapi.io/community to discuss SMAPI.
-
--->
diff --git a/.github/ISSUE_TEMPLATE/general.md b/.github/ISSUE_TEMPLATE/general.md
deleted file mode 100644
index f02d3c9a..00000000
--- a/.github/ISSUE_TEMPLATE/general.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-name: General
-about: Create a ticket about something else.
-
----
-
-<!--
-
-GitHub issues are only used for development tasks.
-For support and questions, see https://smapi.io/community instead.
-
--->
diff --git a/build/common.targets b/build/common.targets
index 512107a0..1ead1508 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
- <Version>3.18.1</Version>
+ <Version>3.18.3</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh
index 1d805e00..304579b9 100755
--- a/build/unix/prepare-install-package.sh
+++ b/build/unix/prepare-install-package.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#
#
diff --git a/build/unix/set-smapi-version.sh b/build/unix/set-smapi-version.sh
index 0c0cbeb0..02b5e615 100755
--- a/build/unix/set-smapi-version.sh
+++ b/build/unix/set-smapi-version.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#
#
diff --git a/build/windows/finalize-install-package.sh b/build/windows/finalize-install-package.sh
index 0996e3ed..117e33e5 100755
--- a/build/windows/finalize-install-package.sh
+++ b/build/windows/finalize-install-package.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
##########
## Read config
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 54cbf96c..d6d3d1cd 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -8,8 +8,38 @@
-->
## Upcoming release
+* For mod authors:
+ * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!).
+
+## 3.18.3
+Released 09 April 2023 for Stardew Valley 1.5.6 or later.
+
+* For players:
+ * Fixed `findstr` installer error for some players.
+ * Fixed installer error for some Linux players due to a non-portable shebang (thanks to freyacoded!).
+ * Fixed error using load order overrides when there are broken mods installed (thanks to atravita!).
+ * Removed `LargeAddressAware` flag on SMAPI (no longer needed since it's 64-bit now).
+ * Improved translations. Thganks to stylemate (updated Korean)!
+
+* For mod authors:
+ * Added `IsActiveForScreen()` method to `PerScreen<T>`.
+ * Updated to [FluentHttpClient](https://github.com/Pathoschild/FluentHttpClient#readme) 4.3.0 (see [changes](https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#430)).
+ * Adjusted `ModContentManager.HandleUnknownFileType` to let mods patch it.
+ * Fixed `Context.IsWorldReady` being editable by mods.
+
+* For the web UI:
+ * Updated the JSON validator/schema for Content Patcher 1.29.0.
+
+## 3.18.2
+Released 09 January 2023 for Stardew Valley 1.5.6 or later.
+
* For players:
- * Fixed save backups being empty in rare cases on macOS.
+ * Fixed empty save backups for some macOS players.
+ * Fixed `player_add` console command not handling custom slingshots correctly (thanks too DaLion!).
+
+* For mod authors:
+ * Added `DelegatingModHooks` utility for mods which need to override SMAPI's mod hooks directly.
+ * Updated to Newtonsoft.Json 13.0.2 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.2)) and Pintail 2.2.2 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#222)).
## 3.18.1
Released 01 December 2022 for Stardew Valley 1.5.6 or later.
diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md
index 0e710e50..ab8a2c95 100644
--- a/docs/technical/mod-package.md
+++ b/docs/technical/mod-package.md
@@ -99,6 +99,10 @@ There are two places you can put them:
2. Open the `.csproj` file in a text editor (Notepad is fine).
3. Add the properties between the first `<PropertyGroup>` and `</PropertyGroup>` tags you find.
+**Note:** you can't use a property before it's defined. That mainly means that when setting
+`GameModsPath`, you'll need to either specify `GamePath` manually or put the full path in
+`GameModsPath`.
+
### Available properties
These are the options you can set:
@@ -412,9 +416,11 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
when you compile it.
## Release notes
-## Upcoming release
+### 4.1.0
+Released 08 January 2023.
+
* Added `manifest.json` format validation on build (thanks to tylergibbs2!).
-* Fixed game assemblies no longer excluded from the release zip if referenced explicitly without setting `BundleExtraAssemblies`.
+* Fixed game DLLs not excluded from the release zip when they're referenced explicitly but `BundleExtraAssemblies` isn't set.
### 4.0.2
Released 09 October 2022.
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.ModPageUrl"/>.</summary>
+ public string? ModPageUrl { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="version">The mod's semantic version.</param>
+ /// <param name="modPageUrl">The mod page URL from which to download updates, if different from <see cref="UpdateManifestModModel.ModPageUrl"/>.</param>
+ public UpdateManifestVersionModel(string version, string? modPageUrl)
+ {
+ this.Version = version;
+ this.ModPageUrl = modPageUrl;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
new file mode 100644
index 00000000..27072897
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Pathoschild.Http.Client;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary>
+ internal class UpdateManifestClient : IUpdateManifestClient
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The underlying HTTP client.</summary>
+ private readonly IClient Client;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The unique key for the mod site.</summary>
+ public ModSiteKey SiteKey => ModSiteKey.UpdateManifest;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="userAgent">The user agent for the API client.</param>
+ public UpdateManifestClient(string userAgent)
+ {
+ this.Client = new FluentClient()
+ .SetUserAgent(userAgent);
+
+ this.Client.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ this.Client.Dispose();
+ }
+
+ /// <inheritdoc/>
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method which ensures the annotations are correct.")]
+ public async Task<IModPage?> GetModData(string id)
+ {
+ // get raw update manifest
+ UpdateManifestModel? manifest;
+ try
+ {
+ manifest = await this.Client.GetAsync(id).As<UpdateManifestModel?>();
+ if (manifest is null)
+ return this.GetFormatError(id, "manifest can't be empty");
+ }
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
+ return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}");
+ }
+ catch (Exception ex)
+ {
+ return this.GetFormatError(id, ex.Message);
+ }
+
+ // validate
+ if (!SemanticVersion.TryParse(manifest.Format, out _))
+ return this.GetFormatError(id, $"invalid format version '{manifest.Format}'");
+ foreach (UpdateManifestModModel mod in manifest.Mods.Values)
+ {
+ if (mod is null)
+ return this.GetFormatError(id, "a mod record can't be null");
+ if (string.IsNullOrWhiteSpace(mod.ModPageUrl))
+ return this.GetFormatError(id, $"all mods must have a {nameof(mod.ModPageUrl)} value");
+ foreach (UpdateManifestVersionModel? version in mod.Versions)
+ {
+ if (version is null)
+ return this.GetFormatError(id, "a version record can't be null");
+ if (string.IsNullOrWhiteSpace(version.Version))
+ return this.GetFormatError(id, $"all version records must have a {nameof(version.Version)} field");
+ if (!SemanticVersion.TryParse(version.Version, out _))
+ return this.GetFormatError(id, $"invalid mod version '{version.Version}'");
+ }
+ }
+
+ // build model
+ return new UpdateManifestModPage(id, manifest);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a mod page instance with an error indicating the update manifest is invalid.</summary>
+ /// <param name="url">The full URL to the update manifest.</param>
+ /// <param name="reason">A human-readable reason phrase indicating why it's invalid.</param>
+ private IModPage GetFormatError(string url, string reason)
+ {
+ return new GenericModPage(this.SiteKey, url).SetError(RemoteModStatus.InvalidData, $"The update manifest at {url} is invalid ({reason})");
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
new file mode 100644
index 00000000..f8cb760a
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
@@ -0,0 +1,34 @@
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>Metadata about a mod download in an update manifest file.</summary>
+ internal class UpdateManifestModDownload : GenericModDownload
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The update subkey for this mod download.</summary>
+ private readonly string Subkey;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fieldName">The field name for this mod download in the manifest.</param>
+ /// <param name="name">The mod name for this download.</param>
+ /// <param name="version">The download's version.</param>
+ /// <param name="url">The download's URL.</param>
+ public UpdateManifestModDownload(string fieldName, string name, string? version, string? url)
+ : base(name, null, version, url)
+ {
+ this.Subkey = '@' + fieldName;
+ }
+
+ /// <summary>Get whether the subkey matches this download.</summary>
+ /// <param name="subkey">The update subkey to check.</param>
+ public override bool MatchesSubkey(string subkey)
+ {
+ return subkey == this.Subkey;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
new file mode 100644
index 00000000..df752713
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>Metadata about an update manifest "page".</summary>
+ internal class UpdateManifestModPage : GenericModPage
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The mods from the update manifest.</summary>
+ private readonly IDictionary<string, UpdateManifestModModel> Mods;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="url">The URL of the update manifest file.</param>
+ /// <param name="manifest">The parsed update manifest.</param>
+ public UpdateManifestModPage(string url, UpdateManifestModel manifest)
+ : base(ModSiteKey.UpdateManifest, url)
+ {
+ this.RequireSubkey = true;
+ this.Mods = manifest.Mods;
+ this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray());
+ }
+
+ /// <summary>Return the mod name for the given subkey, if it exists in this update manifest.</summary>
+ /// <param name="subkey">The subkey.</param>
+ /// <returns>The mod name for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns>
+ public override string? GetName(string? subkey)
+ {
+ return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod)
+ ? mod.Name
+ : null;
+ }
+
+ /// <summary>Return the mod URL for the given subkey, if it exists in this update manifest.</summary>
+ /// <param name="subkey">The subkey.</param>
+ /// <returns>The mod URL for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns>
+ public override string? GetUrl(string? subkey)
+ {
+ return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod)
+ ? mod.ModPageUrl
+ : null;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Convert the raw download info from an update manifest to <see cref="IModDownload"/>.</summary>
+ /// <param name="mods">The mods from the update manifest.</param>
+ private IEnumerable<IModDownload> ParseDownloads(IDictionary<string, UpdateManifestModModel>? mods)
+ {
+ if (mods is null)
+ yield break;
+
+ foreach ((string modKey, UpdateManifestModModel mod) in mods)
+ {
+ foreach (UpdateManifestVersionModel version in mod.Versions)
+ yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.ModPageUrl);
+ }
+ }
+
+ }
+}
diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs
index fe171785..8cb82989 100644
--- a/src/SMAPI.Web/Framework/IModDownload.cs
+++ b/src/SMAPI.Web/Framework/IModDownload.cs
@@ -14,5 +14,16 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The download's file version.</summary>
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>
+ string? ModPageUrl { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get whether the subkey matches this download.</summary>
+ /// <param name="subkey">The update subkey to check.</param>
+ bool MatchesSubkey(string subkey);
}
}
diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs
index 4d0a8d61..85be41e2 100644
--- a/src/SMAPI.Web/Framework/IModPage.cs
+++ b/src/SMAPI.Web/Framework/IModPage.cs
@@ -39,10 +39,21 @@ namespace StardewModdingAPI.Web.Framework
[MemberNotNullWhen(false, nameof(IModPage.Error))]
bool IsValid { get; }
+ /// <summary>Whether this mod page requires update subkeys and does not allow matching downloads without them.</summary>
+ bool RequireSubkey { get; }
+
/*********
** Methods
*********/
+ /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary>
+ /// <param name="subkey">The update subkey.</param>
+ string? GetName(string? subkey);
+
+ /// <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>
+ string? GetUrl(string? subkey);
+
/// <summary>Set the fetched mod info.</summary>
/// <param name="name">The mod name.</param>
/// <param name="version">The mod's semantic version number.</param>
diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs
index e70b60bf..502c0827 100644
--- a/src/SMAPI.Web/Framework/ModInfoModel.cs
+++ b/src/SMAPI.Web/Framework/ModInfoModel.cs
@@ -27,6 +27,12 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
public string? Error { get; private set; }
+ /// <summary>The mod page URL from which <see cref="Version"/> can be downloaded, if different from the <see cref="Url"/>.</summary>
+ public string? MainModPageUrl { get; private set; }
+
+ /// <summary>The mod page URL from which <see cref="PreviewVersion"/> can be downloaded, if different from the <see cref="Url"/>.</summary>
+ public string? PreviewModPageUrl { get; private set; }
+
/*********
** Public methods
@@ -46,7 +52,8 @@ namespace StardewModdingAPI.Web.Framework
{
this
.SetBasicInfo(name, url)
- .SetVersions(version!, previewVersion)
+ .SetMainVersion(version!)
+ .SetPreviewVersion(previewVersion)
.SetError(status, error!);
}
@@ -62,14 +69,25 @@ namespace StardewModdingAPI.Web.Framework
return this;
}
- /// <summary>Set the mod version info.</summary>
- /// <param name="version">The semantic version for the mod's latest release.</param>
- /// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
+ /// <summary>Set the mod's main version info.</summary>
+ /// <param name="version">The semantic version for the mod's latest stable release.</param>
+ /// <param name="modPageUrl">The mod page URL from which <paramref name="version"/> can be downloaded, if different from the <see cref="Url"/>.</param>
[MemberNotNull(nameof(ModInfoModel.Version))]
- public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null)
+ public ModInfoModel SetMainVersion(ISemanticVersion version, string? modPageUrl = null)
{
this.Version = version;
- this.PreviewVersion = previewVersion;
+ this.MainModPageUrl = modPageUrl;
+
+ return this;
+ }
+
+ /// <summary>Set the mod's preview version info.</summary>
+ /// <param name="version">The semantic version for the mod's latest preview release.</param>
+ /// <param name="modPageUrl">The mod page URL from which <paramref name="version"/> can be downloaded, if different from the <see cref="Url"/>.</param>
+ public ModInfoModel SetPreviewVersion(ISemanticVersion? version, string? modPageUrl = null)
+ {
+ this.PreviewVersion = version;
+ this.PreviewModPageUrl = modPageUrl;
return this;
}
diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs
index 674b9ffc..4bb72f78 100644
--- a/src/SMAPI.Web/Framework/ModSiteManager.cs
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -59,30 +59,42 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Parse version info for the given mod page info.</summary>
/// <param name="page">The mod page info.</param>
- /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
+ /// <param name="updateKey">The update key to match in available files.</param>
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
- public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
+ public ModInfoModel GetPageVersions(IModPage page, UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
{
- // get base model
+ // get ID to show in errors
+ string displayId = page.RequireSubkey
+ ? page.Id + updateKey.Subkey
+ : page.Id;
+
+ // validate
ModInfoModel model = new();
- if (page.IsValid)
+ if (!page.IsValid)
+ return model.SetError(page.Status, page.Error);
+ if (page.RequireSubkey && updateKey.Subkey is null)
+ return model.SetError(RemoteModStatus.RequiredSubkeyMissing, $"The {page.Site} mod with ID '{displayId}' requires an update subkey indicating which mod to fetch.");
+
+ // add basic info (unless it's a manifest, in which case the 'mod page' is the JSON file)
+ if (updateKey.Site != ModSiteKey.UpdateManifest)
model.SetBasicInfo(page.Name, page.Url);
- else
- {
- model.SetError(page.Status, page.Error);
- return model;
- }
// fetch versions
- bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion);
- if (!hasVersions && subkey != null)
- hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion);
+ bool hasVersions = this.TryGetLatestVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl);
if (!hasVersions)
- return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions.");
+ return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{displayId}' has no valid versions.");
+
+ // apply mod page info
+ model.SetBasicInfo(
+ name: page.GetName(updateKey.Subkey) ?? page.Name,
+ url: page.GetUrl(updateKey.Subkey) ?? page.Url
+ );
// return info
- return model.SetVersions(mainVersion!, previewVersion);
+ return model
+ .SetMainVersion(mainVersion!, mainModPageUrl)
+ .SetPreviewVersion(previewVersion, previewModPageUrl);
}
/// <summary>Get a semantic local version for update checks.</summary>
@@ -113,34 +125,37 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="main">The main mod version.</param>
/// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param>
- private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview)
+ /// <param name="mainModPageUrl">The mod page URL from which <paramref name="main"/> can be downloaded, if different from the <see cref="mod"/>'s URL.</param>
+ /// <param name="previewModPageUrl">The mod page URL from which <paramref name="preview"/> can be downloaded, if different from the <see cref="mod"/>'s URL.</param>
+ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainModPageUrl, out string? previewModPageUrl)
{
main = null;
preview = null;
+ mainModPageUrl = null;
+ previewModPageUrl = null;
+ if (mod is null)
+ return false;
// parse all versions from the mod page
- IEnumerable<(string? name, string? description, ISemanticVersion? version)> GetAllVersions()
+ IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions()
{
- if (mod != null)
+ ISemanticVersion? ParseAndMapVersion(string? raw)
{
- ISemanticVersion? ParseAndMapVersion(string? raw)
- {
- raw = this.NormalizeVersion(raw);
- return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
- }
+ raw = this.NormalizeVersion(raw);
+ return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
+ }
- // get mod version
- ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version);
- if (modVersion != null)
- yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version));
+ // get mod version
+ ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version);
+ if (modVersion != null)
+ yield return (download: null, version: modVersion);
- // get file versions
- foreach (IModDownload download in mod.Downloads)
- {
- ISemanticVersion? cur = ParseAndMapVersion(download.Version);
- if (cur != null)
- yield return (download.Name, download.Description, cur);
- }
+ // get file versions
+ foreach (IModDownload download in mod.Downloads)
+ {
+ ISemanticVersion? cur = ParseAndMapVersion(download.Version);
+ if (cur != null)
+ yield return (download, cur);
}
}
var versions = GetAllVersions()
@@ -148,40 +163,59 @@ namespace StardewModdingAPI.Web.Framework
.ToArray();
// get main + preview versions
- void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(string? name, string? description, ISemanticVersion? version), bool>? filter = null)
+ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainUrl, out string? previewUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null)
{
mainVersion = null;
previewVersion = null;
+ mainUrl = null;
+ previewUrl = null;
// get latest main + preview version
- foreach ((string? name, string? description, ISemanticVersion? version) entry in versions)
+ foreach ((IModDownload? download, ISemanticVersion? version) entry in versions)
{
if (entry.version is null || filter?.Invoke(entry) == false)
continue;
if (entry.version.IsPrerelease())
- previewVersion ??= entry.version;
+ {
+ if (previewVersion is null)
+ {
+ previewVersion = entry.version;
+ previewUrl = entry.download?.ModPageUrl;
+ }
+ }
else
- mainVersion ??= entry.version;
-
- if (mainVersion != null)
+ {
+ mainVersion = entry.version;
+ mainUrl = entry.download?.ModPageUrl;
break; // any others will be older since entries are sorted by version
+ }
}
// normalize values
if (previewVersion is not null)
{
- mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version
+ if (mainVersion is null)
+ {
+ // if every version is prerelease, latest one is the main version
+ mainVersion = previewVersion;
+ mainUrl = previewUrl;
+ }
if (!previewVersion.IsNewerThan(mainVersion))
+ {
previewVersion = null;
+ previewUrl = null;
+ }
}
}
+ // get versions for subkey
if (subkey is not null)
- TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true);
- if (main is null)
- TryGetVersions(out main, out preview);
+ TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true);
+ // fallback to non-subkey versions
+ if (main is null && !mod.RequireSubkey)
+ TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl);
return main != null;
}
diff --git a/src/SMAPI.Web/Framework/RemoteModStatus.cs b/src/SMAPI.Web/Framework/RemoteModStatus.cs
index 139ecfd3..235bcec4 100644
--- a/src/SMAPI.Web/Framework/RemoteModStatus.cs
+++ b/src/SMAPI.Web/Framework/RemoteModStatus.cs
@@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The mod does not exist.</summary>
DoesNotExist,
+ /// <summary>The mod page exists, but it requires a subkey and none was provided.</summary>
+ RequiredSubkeyMissing,
+
/// <summary>The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred).</summary>
TemporaryError
}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index 81b187fe..1e568572 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>SMAPI.Web</AssemblyName>
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
- <TargetFramework>net6.0</TargetFramework>
+ <TargetFramework>net7.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
@@ -15,17 +15,17 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
- <PackageReference Include="Hangfire.AspNetCore" Version="1.7.29" />
+ <PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
+ <PackageReference Include="Hangfire.AspNetCore" Version="1.7.32" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
- <PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
+ <PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
- <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
- <PackageReference Include="Markdig" Version="0.30.2" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.5" />
+ <PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
+ <PackageReference Include="Markdig" Version="0.30.4" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Pathoschild.FluentNexus" Version="1.0.5" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 54c25979..a068a998 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -20,6 +20,7 @@ using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
+using StardewModdingAPI.Web.Framework.Clients.UpdateManifest;
using StardewModdingAPI.Web.Framework.Compression;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.RedirectRules;
@@ -149,6 +150,8 @@ namespace StardewModdingAPI.Web
baseUrl: api.PastebinBaseUrl,
userAgent: userAgent
));
+
+ services.AddSingleton<IUpdateManifestClient>(new UpdateManifestClient(userAgent: userAgent));
}
// init helpers
diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
index a00403c0..bd9e7427 100644
--- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
+++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
@@ -14,9 +14,9 @@
"title": "Format version",
"description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.",
"type": "string",
- "pattern": "^1\\.28\\.[0-9]+$",
+ "pattern": "^1\\.29\\.[0-9]+$",
"@errorMessages": {
- "pattern": "Incorrect value '@value'. You should always use the latest format version (currently 1.28.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
+ "pattern": "Incorrect value '@value'. You should always use the latest format version (currently 1.29.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
}
},
"ConfigSchema": {
diff --git a/src/SMAPI.sln b/src/SMAPI.sln
index 8bf86487..99b9dc83 100644
--- a/src/SMAPI.sln
+++ b/src/SMAPI.sln
@@ -19,9 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{4B1C
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{F4453AB6-D7D6-447F-A973-956CC777968F}"
ProjectSection(SolutionItems) = preProject
- ..\.github\ISSUE_TEMPLATE\bug_report.md = ..\.github\ISSUE_TEMPLATE\bug_report.md
- ..\.github\ISSUE_TEMPLATE\feature_request.md = ..\.github\ISSUE_TEMPLATE\feature_request.md
- ..\.github\ISSUE_TEMPLATE\general.md = ..\.github\ISSUE_TEMPLATE\general.md
+ ..\.github\ISSUE_TEMPLATE\config.yml = ..\.github\ISSUE_TEMPLATE\config.yml
+ ..\.github\ISSUE_TEMPLATE\custom.md = ..\.github\ISSUE_TEMPLATE\custom.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}"
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 3ff3159b..e5601747 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -52,7 +52,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.18.1";
+ internal static string RawApiVersion = "3.18.3";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs
index c822908e..978459e8 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -60,7 +60,7 @@ namespace StardewModdingAPI
public static bool IsWorldReady
{
get => Context.IsWorldReadyForScreen.Value;
- set => Context.IsWorldReadyForScreen.Value = value;
+ internal set => Context.IsWorldReadyForScreen.Value = value;
}
/// <summary>Whether <see cref="IsWorldReady"/> is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc).</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index badbd766..2c068784 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -130,7 +130,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
".png" => this.LoadImageFile<T>(assetName, file),
".tbin" or ".tmx" => this.LoadMapFile<T>(assetName, file),
".xnb" => this.LoadXnbFile<T>(assetName),
- _ => this.HandleUnknownFileType<T>(assetName, file)
+ _ => (T)this.HandleUnknownFileType(assetName, file, typeof(T))
};
}
catch (Exception ex)
@@ -323,13 +323,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
/// <summary>Handle a request to load a file type that isn't supported by SMAPI.</summary>
- /// <typeparam name="T">The expected file type.</typeparam>
/// <param name="assetName">The asset name relative to the loader root directory.</param>
/// <param name="file">The file to load.</param>
- private T HandleUnknownFileType<T>(IAssetName assetName, FileInfo file)
+ /// <param name="assetType">The expected file type.</param>
+ private object HandleUnknownFileType(IAssetName assetName, FileInfo file, Type assetType)
{
this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
- return default;
+ return assetType.IsValueType
+ ? Activator.CreateInstance(assetType)
+ : null;
}
/// <summary>Assert that the asset type is compatible with one of the allowed types.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index cb62e16f..607bb70d 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -180,13 +180,16 @@ namespace StardewModdingAPI.Framework.ModLoading
return mods
.OrderBy(mod =>
{
- string id = mod.Manifest.UniqueID;
+ string? id = mod.Manifest?.UniqueID;
- if (modIdsToLoadEarly.TryGetValue(id, out string? actualId))
- return -int.MaxValue + Array.IndexOf(earlyArray, actualId);
+ if (id is not null)
+ {
+ if (modIdsToLoadEarly.TryGetValue(id, out string? actualId))
+ return -int.MaxValue + Array.IndexOf(earlyArray, actualId);
- if (modIdsToLoadLate.TryGetValue(id, out actualId))
- return int.MaxValue - Array.IndexOf(lateArray, actualId);
+ if (modIdsToLoadLate.TryGetValue(id, out actualId))
+ return int.MaxValue - Array.IndexOf(lateArray, actualId);
+ }
return 0;
})
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index fea0f7d0..3e179ef7 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -260,7 +260,11 @@ namespace StardewModdingAPI.Framework
monitor: this.Monitor,
reflection: this.Reflection,
eventManager: this.EventManager,
- modHooks: new SModHooks(this.OnNewDayAfterFade, this.Monitor),
+ modHooks: new SModHooks(
+ parent: new ModHooks(),
+ beforeNewDayAfterFade: this.OnNewDayAfterFade,
+ monitor: this.Monitor
+ ),
multiplayer: this.Multiplayer,
exitGameImmediately: this.ExitGameImmediately,
@@ -431,7 +435,7 @@ namespace StardewModdingAPI.Framework
// apply load order customizations
if (this.Settings.ModsToLoadEarly.Any() || this.Settings.ModsToLoadLate.Any())
{
- HashSet<string> installedIds = new HashSet<string>(mods.Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
+ HashSet<string> installedIds = new HashSet<string>(mods.Select(p => p.Manifest?.UniqueID).Where(p => p is not null), StringComparer.OrdinalIgnoreCase);
string[] missingEarlyMods = this.Settings.ModsToLoadEarly.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
string[] missingLateMods = this.Settings.ModsToLoadLate.Where(id => !installedIds.Contains(id)).OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToArray();
@@ -1797,7 +1801,7 @@ namespace StardewModdingAPI.Framework
// call entry method
try
{
- mod.Entry(mod.Helper!);
+ mod.Entry(mod.Helper);
}
catch (Exception ex)
{
@@ -1824,7 +1828,7 @@ namespace StardewModdingAPI.Framework
}
// validate mod doesn't implement both GetApi() and GetApi(mod)
- if (metadata.Api != null && mod.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IModInfo) })!.DeclaringType != typeof(Mod))
+ if (metadata.Api != null && mod.GetType().GetMethod(nameof(Mod.GetApi), new[] { typeof(IModInfo) })!.DeclaringType != typeof(Mod))
metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IModInfo)}), which isn't allowed. The latter will be ignored.", LogLevel.Error);
}
Context.HeuristicModsRunningCode.TryPop(out _);
diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs
index a7736c8b..ac4f242c 100644
--- a/src/SMAPI/Framework/SModHooks.cs
+++ b/src/SMAPI/Framework/SModHooks.cs
@@ -1,11 +1,12 @@
using System;
using System.Threading.Tasks;
+using StardewModdingAPI.Utilities;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// <summary>Invokes callbacks for mod hooks provided by the game.</summary>
- internal class SModHooks : ModHooks
+ internal class SModHooks : DelegatingModHooks
{
/*********
** Fields
@@ -21,25 +22,24 @@ namespace StardewModdingAPI.Framework
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <param name="parent">The underlying hooks to call by default.</param>
/// <param name="beforeNewDayAfterFade">A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</param>
/// <param name="monitor">Writes messages to the console.</param>
- public SModHooks(Action beforeNewDayAfterFade, IMonitor monitor)
+ public SModHooks(ModHooks parent, Action beforeNewDayAfterFade, IMonitor monitor)
+ : base(parent)
{
this.BeforeNewDayAfterFade = beforeNewDayAfterFade;
this.Monitor = monitor;
}
- /// <summary>A hook invoked when <see cref="Game1.newDayAfterFade"/> is called.</summary>
- /// <param name="action">The vanilla <see cref="Game1.newDayAfterFade"/> logic.</param>
+ /// <inheritdoc />
public override void OnGame1_NewDayAfterFade(Action action)
{
this.BeforeNewDayAfterFade();
action();
}
- /// <summary>Start an asynchronous task for the game.</summary>
- /// <param name="task">The task to start.</param>
- /// <param name="id">A unique key which identifies the task.</param>
+ /// <inheritdoc />
public override Task StartTask(Task task, string id)
{
this.Monitor.Log($"Synchronizing '{id}' task...");
@@ -48,9 +48,7 @@ namespace StardewModdingAPI.Framework
return task;
}
- /// <summary>Start an asynchronous task for the game.</summary>
- /// <param name="task">The task to start.</param>
- /// <param name="id">A unique key which identifies the task.</param>
+ /// <inheritdoc />
public override Task<T> StartTask<T>(Task<T> task, string id)
{
this.Monitor.Log($"Synchronizing '{id}' task...");
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index e5d8937c..530b75aa 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -8,7 +8,6 @@
<OutputType>Exe</OutputType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
- <LargeAddressAware Condition="'$(OS)' == 'Windows_NT'">true</LargeAddressAware>
<ApplicationIcon>icon.ico</ApplicationIcon>
<!--copy dependency DLLs to bin folder so we can include them in installer bundle -->
@@ -22,12 +21,11 @@
<Import Project="..\..\build\common.targets" />
<ItemGroup>
- <PackageReference Include="LargeAddressAware" Version="1.0.5" />
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="MonoMod.Common" Version="22.3.5.1" />
- <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
- <PackageReference Include="Pintail" Version="2.2.1" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.3.0" />
+ <PackageReference Include="Pintail" Version="2.2.2" />
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
diff --git a/src/SMAPI/Utilities/DelegatingModHooks.cs b/src/SMAPI/Utilities/DelegatingModHooks.cs
new file mode 100644
index 00000000..3ebcf997
--- /dev/null
+++ b/src/SMAPI/Utilities/DelegatingModHooks.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Xna.Framework.Input;
+using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework;
+using StardewValley;
+using StardewValley.Events;
+
+namespace StardewModdingAPI.Utilities
+{
+ /// <summary>An implementation of <see cref="ModHooks"/> which automatically calls the parent instance for any method that's not overridden.</summary>
+ /// <remarks>The mod hooks are primarily meant for SMAPI to use. Using this directly in mods is a last resort, since it's very easy to break SMAPI this way. This class requires that SMAPI is present in the parent chain.</remarks>
+ public class DelegatingModHooks : ModHooks
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The underlying instance to delegate to by default.</summary>
+ public ModHooks Parent { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modHooks">The underlying instance to delegate to by default.</param>
+ public DelegatingModHooks(ModHooks modHooks)
+ {
+ this.AssertSmapiInChain(modHooks);
+
+ this.Parent = modHooks;
+ }
+
+ /// <summary>Raised before the in-game clock changes.</summary>
+ /// <param name="action">Run the vanilla update logic.</param>
+ /// <remarks>In mods, consider using <see cref="IGameLoopEvents.TimeChanged"/> instead.</remarks>
+ public override void OnGame1_PerformTenMinuteClockUpdate(Action action)
+ {
+ this.Parent.OnGame1_PerformTenMinuteClockUpdate(action);
+ }
+
+ /// <summary>Raised before initializing the new day and saving.</summary>
+ /// <param name="action">Run the vanilla update logic.</param>
+ /// <remarks>In mods, consider using <see cref="IGameLoopEvents.DayEnding"/> or <see cref="IGameLoopEvents.Saving"/> instead.</remarks>
+ public override void OnGame1_NewDayAfterFade(Action action)
+ {
+ this.Parent.OnGame1_NewDayAfterFade(action);
+ }
+
+ /// <summary>Raised before showing the end-of-day menus (e.g. shipping menus, level-up screen, etc).</summary>
+ /// <param name="action">Run the vanilla update logic.</param>
+ public override void OnGame1_ShowEndOfNightStuff(Action action)
+ {
+ this.Parent.OnGame1_ShowEndOfNightStuff(action);
+ }
+
+ /// <summary>Raised before updating the gamepad, mouse, and keyboard input state.</summary>
+ /// <param name="keyboardState">The keyboard state.</param>
+ /// <param name="mouseState">The mouse state.</param>
+ /// <param name="gamePadState">The gamepad state.</param>
+ /// <param name="action">Run the vanilla update logic.</param>
+ /// <remarks>In mods, consider using <see cref="IInputEvents"/> instead.</remarks>
+ public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action)
+ {
+ this.Parent.OnGame1_UpdateControlInput(ref keyboardState, ref mouseState, ref gamePadState, action);
+ }
+
+ /// <summary>Raised before a location is updated for the local player entering it.</summary>
+ /// <param name="location">The location that will be updated.</param>
+ /// <param name="action">Run the vanilla update logic.</param>
+ /// <remarks>In mods, consider using <see cref="IPlayerEvents.Warped"/> instead.</remarks>
+ public override void OnGameLocation_ResetForPlayerEntry(GameLocation location, Action action)
+ {
+ this.Parent.OnGameLocation_ResetForPlayerEntry(location, action);
+ }
+
+ /// <summary>Raised before the game checks for an action to trigger for a player interaction with a tile.</summary>
+ /// <param name="location">The location being checked.</param>
+ /// <param name="tileLocation">The tile position being checked.</param>
+ /// <param name="viewport">The game's current position and size within the map, measured in pixels.</param>
+ /// <param name="who">The player interacting with the tile.</param>
+ /// <param name="action">Run the vanilla update logic.</param>
+ /// <returns>Returns whether the interaction was handled.</returns>
+ public override bool OnGameLocation_CheckAction(GameLocation location, xTile.Dimensions.Location tileLocation, xTile.Dimensions.Rectangle viewport, Farmer who, Func<bool> action)
+ {
+ return this.Parent.OnGameLocation_CheckAction(location, tileLocation, viewport, who, action);
+ }
+
+ /// <summary>Raised before the game picks a night event to show on the farm after the player sleeps.</summary>
+ /// <param name="action">Run the vanilla update logic.</param>
+ /// <returns>Returns the selected farm event.</returns>
+ public override FarmEvent OnUtility_PickFarmEvent(Func<FarmEvent> action)
+ {
+ return this.Parent.OnUtility_PickFarmEvent(action);
+ }
+
+ /// <summary>Start an asynchronous task for the game.</summary>
+ /// <param name="task">The task to start.</param>
+ /// <param name="id">A unique key which identifies the task.</param>
+ public override Task StartTask(Task task, string id)
+ {
+ return this.Parent.StartTask(task, id);
+ }
+
+ /// <summary>Start an asynchronous task for the game.</summary>
+ /// <typeparam name="T">The type returned by the task when it completes.</typeparam>
+ /// <param name="task">The task to start.</param>
+ /// <param name="id">A unique key which identifies the task.</param>
+ public override Task<T> StartTask<T>(Task<T> task, string id)
+ {
+ return this.Parent.StartTask<T>(task, id);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Assert that SMAPI's mod hook implementation is in the inheritance chain.</summary>
+ /// <param name="hooks">The mod hooks to check.</param>
+ private void AssertSmapiInChain(ModHooks hooks)
+ {
+ // this is SMAPI
+ if (this is SModHooks)
+ return;
+
+ // SMAPI in delegated chain
+ for (ModHooks? cur = hooks; cur != null; cur = (cur as DelegatingModHooks)?.Parent)
+ {
+ if (cur is SModHooks)
+ return;
+ }
+
+ // SMAPI not found
+ throw new InvalidOperationException($"Can't create a {nameof(DelegatingModHooks)} instance without SMAPI's mod hooks in the parent chain.");
+ }
+ }
+}
diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs
index 87bf2027..674ec760 100644
--- a/src/SMAPI/Utilities/PerScreen.cs
+++ b/src/SMAPI/Utilities/PerScreen.cs
@@ -101,6 +101,12 @@ namespace StardewModdingAPI.Utilities
this.RemoveScreens(_ => true);
}
+ /// <summary>Get whether the current screen has a value created yet.</summary>
+ public bool IsActiveForScreen()
+ {
+ return this.States.ContainsKey(Context.ScreenId);
+ }
+
/*********
** Private methods
diff --git a/src/SMAPI/i18n/ko.json b/src/SMAPI/i18n/ko.json
index 8d267e5e..8122a9e2 100644
--- a/src/SMAPI/i18n/ko.json
+++ b/src/SMAPI/i18n/ko.json
@@ -1,6 +1,6 @@
{
// short date format for SDate
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
- "generic.date": "{{season}} {{day}}",
- "generic.date-with-year": "{{year}} 학년 {{season}} {{day}}"
+ "generic.date": "{{season}} {{day}}일",
+ "generic.date-with-year": "{{year}}년차 {{season}} {{day}}일"
}