summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets2
-rw-r--r--build/find-game-folder.targets22
-rw-r--r--docs/README.md38
-rw-r--r--docs/release-notes.md26
-rw-r--r--docs/technical/mod-package.md3
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs1
-rw-r--r--src/SMAPI.Installer/assets/unix-launcher.sh125
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs30
-rw-r--r--src/SMAPI.Mods.ErrorHandler/i18n/uk.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs11
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs33
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs35
-rw-r--r--src/SMAPI.Web/ViewModels/LogViewFormat.cs15
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml133
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/log-parser.css26
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/log-parser.js11
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json7
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/manifest.json2
-rw-r--r--src/SMAPI.sln.DotSettings1
-rw-r--r--src/SMAPI/Constants.cs15
-rw-r--r--src/SMAPI/Framework/Content/TilesheetReference.cs15
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs39
-rw-r--r--src/SMAPI/Framework/Logging/InterceptingTextWriter.cs40
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs37
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs24
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs54
-rw-r--r--src/SMAPI/Framework/Monitor.cs2
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs9
-rw-r--r--src/SMAPI/i18n/uk.json6
33 files changed, 494 insertions, 288 deletions
diff --git a/build/common.targets b/build/common.targets
index 1021c2a1..8eac9757 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
- <Version>3.13.2</Version>
+ <Version>3.13.3</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
diff --git a/build/find-game-folder.targets b/build/find-game-folder.targets
index 3164b071..ba7cb26c 100644
--- a/build/find-game-folder.targets
+++ b/build/find-game-folder.targets
@@ -28,15 +28,31 @@
<_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32))</_SteamLibraryPath>
<GamePath Condition="!Exists('$(GamePath)') AND '$(_SteamLibraryPath)' != ''">$(_SteamLibraryPath)\steamapps\common\Stardew Valley</GamePath>
- <!-- default paths -->
+ <!-- GOG paths -->
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Galaxy\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Games\Stardew Valley</GamePath>
- <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Stardew Valley</GamePath>
-
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Games\Stardew Valley</GamePath>
+
+ <!-- Xbox app paths -->
+ <!--
+ The Xbox app saves the install path to the registry, but we can't use it here since it
+ saves the internal readonly path (like C:\Program Files\WindowsApps\Mutable\<package ID>)
+ instead of the mods-enabled path (like C:\Program Files\ModifiableWindowsApps\Stardew Valley).
+ Fortunately we can cheat a bit: players can customize the install drive, but they can't
+ change the install path on the drive.
+ -->
+ <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
+ <GamePath Condition="!Exists('$(GamePath)')">D:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
+ <GamePath Condition="!Exists('$(GamePath)')">E:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
+ <GamePath Condition="!Exists('$(GamePath)')">F:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
+ <GamePath Condition="!Exists('$(GamePath)')">G:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
+ <GamePath Condition="!Exists('$(GamePath)')">H:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
+
+ <!-- Steam paths -->
+ <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath>
</PropertyGroup>
</When>
diff --git a/docs/README.md b/docs/README.md
index ecfa6f2b..d3aaae64 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -56,22 +56,24 @@ SMAPI rarely shows text in-game, so it only has a few translations. Contribution
[Modding:Translations](https://stardewvalleywiki.com/Modding:Translations) on the wiki for help
contributing translations.
-locale | status
----------- | :----------------
-default | ✓ [fully translated](../src/SMAPI/i18n/default.json)
-Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json)
-French | ✓ [fully translated](../src/SMAPI/i18n/fr.json)
-German | ✓ [fully translated](../src/SMAPI/i18n/de.json)
-Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json)
-Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json)
-Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json)
-Korean | ✓ [fully translated](../src/SMAPI/i18n/ko.json)
-Polish¹ | ✓ [fully translated](../src/SMAPI/i18n/pl.json)
-Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json)
-Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json)
-Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json)
-Thai¹ | ✓ [fully translated](../src/SMAPI/i18n/th.json)
-Turkish | ✓ [fully translated](../src/SMAPI/i18n/tr.json)
+locale | status
+----------- | :----------------
+default | ✓ [fully translated](../src/SMAPI/i18n/default.json)
+Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json)
+French | ✓ [fully translated](../src/SMAPI/i18n/fr.json)
+German | ✓ [fully translated](../src/SMAPI/i18n/de.json)
+Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json)
+Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json)
+Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json)
+Korean | ✓ [fully translated](../src/SMAPI/i18n/ko.json)
+[Polish] | ✓ [fully translated](../src/SMAPI/i18n/pl.json)
+Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json)
+Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json)
+Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json)
+[Thai] | ✓ [fully translated](../src/SMAPI/i18n/th.json)
+Turkish | ✓ [fully translated](../src/SMAPI/i18n/tr.json)
+[Ukrainian] | ✓ [fully translated](../src/SMAPI/i18n/uk.json)
-¹ This is a custom language provided by a mod (see [Polish](https://www.nexusmods.com/stardewvalley/mods/3616)
-and [Thai](https://www.nexusmods.com/stardewvalley/mods/7052)).
+[Polish]: https://www.nexusmods.com/stardewvalley/mods/3616
+[Thai]: https://www.nexusmods.com/stardewvalley/mods/7052
+[Ukrainian]: https://www.nexusmods.com/stardewvalley/mods/8427
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 499fa322..957d5199 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,6 +1,32 @@
← [README](README.md)
# Release notes
+## 3.13.3
+Released 16 January 2021 for Stardew Valley 1.5.6 or later.
+
+* For players:
+ * **SMAPI now needs Stardew Valley 1.5.6 or later.**
+ * Added automatic fix for custom maps which are missing a required tilesheet.
+ * Added automatic save recovery when a custom farm type isn't available anymore.
+ * Added the game's new build number to the SMAPI console + log.
+ * The installer now detects Xbox app game folders.
+ * Reduced mod loading time a bit.
+ * Fixed macOS launch issue when using some terminals (thanks to bruce2409!).
+ * Fixed Linux/macOS terminal ignoring backspaces in Stardew Valley 1.5.5+.
+ * Fixed extra newlines in the SMAPI console.
+ * Fixed outdated instructions in Steam error message.
+ * Fixed uninstaller not removing `StardewModdingAPI.deps.json` file.
+ * Simplified [running without a terminal on Linux/macOS](https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting#SMAPI_doesn.27t_recognize_controller_.28Steam_only.29) when needed.
+ * Updated compatibility list.
+ * Improved translations. Thanks to ChulkyBow (added Ukrainian)!
+
+* For the web UI:
+ * Added log instructions for Xbox app on Windows.
+ * Added log download option.
+ * Redesigned log instruction UI.
+ * Fixed log parser not correctly handling multiple mods having the exact same name.
+ * Fixed JSON validator not recognizing manifest [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys).
+
## 3.13.2
Released 05 December 2021 for Stardew Valley 1.5.5 or later.
diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md
index 41f808a5..5e408168 100644
--- a/docs/technical/mod-package.md
+++ b/docs/technical/mod-package.md
@@ -412,6 +412,9 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
when you compile it.
## Release notes
+## Upcoming release
+* Added detection for Xbox app game folders.
+
## 4.0.0
Released 30 November 2021.
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index 1257f12b..b3bba883 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -41,6 +41,7 @@ namespace StardewModdingApi.Installer
// current files
yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only
+ yield return GetInstallPath("StardewModdingAPI.deps.json");
yield return GetInstallPath("StardewModdingAPI.dll");
yield return GetInstallPath("StardewModdingAPI.exe");
yield return GetInstallPath("StardewModdingAPI.exe.config");
diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh
index e8b9ae62..47937f95 100644
--- a/src/SMAPI.Installer/assets/unix-launcher.sh
+++ b/src/SMAPI.Installer/assets/unix-launcher.sh
@@ -6,6 +6,10 @@
# move to script's directory
cd "$(dirname "$0")" || exit $?
+# change to true to skip opening a terminal
+# This isn't recommended since you won't see errors, warnings, and update alerts.
+SKIP_TERMINAL=false
+
##########
## Open terminal if needed
@@ -16,7 +20,6 @@ cd "$(dirname "$0")" || exit $?
if [ "$(uname)" == "Darwin" ]; then
if [ ! -t 1 ]; then # https://stackoverflow.com/q/911168/262123
# sanity check to make sure we don't have an infinite loop of opening windows
- SKIP_TERMINAL=false
for argument in "$@"; do
if [ "$argument" == "--no-reopen-terminal" ]; then
SKIP_TERMINAL=true
@@ -28,7 +31,8 @@ if [ "$(uname)" == "Darwin" ]; then
# https://stackoverflow.com/a/29511052/262123
if [ "$SKIP_TERMINAL" == "false" ]; then
echo "Reopening in the Terminal app..."
- echo "\"$0\" $@ --no-reopen-terminal" > /tmp/open-smapi-terminal.sh
+ echo '#!/bin/sh' > /tmp/open-smapi-terminal.sh
+ echo "\"$0\" $@ --no-reopen-terminal" >> /tmp/open-smapi-terminal.sh
chmod +x /tmp/open-smapi-terminal.sh
cat /tmp/open-smapi-terminal.sh
open -W -a Terminal /tmp/open-smapi-terminal.sh
@@ -63,64 +67,71 @@ else
LAUNCH_FILE="./StardewModdingAPI"
export LAUNCH_FILE
- # select terminal (prefer xterm for best compatibility, then known supported terminals)
- for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do
- if command -v "$terminal" 2>/dev/null; then
- export TERMINAL_NAME=$terminal
- break;
+ # run in terminal
+ if [ "$SKIP_TERMINAL" == "false" ]; then
+ # select terminal (prefer xterm for best compatibility, then known supported terminals)
+ for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do
+ if command -v "$terminal" 2>/dev/null; then
+ export TERMINAL_NAME=$terminal
+ break;
+ fi
+ done
+
+ # find the true shell behind x-terminal-emulator
+ if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then
+ export TERMINAL_NAME="$(basename "$(readlink -f $(command -v x-terminal-emulator))")"
fi
- done
- # find the true shell behind x-terminal-emulator
- if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then
- export TERMINAL_NAME="$(basename "$(readlink -f $(command -v x-terminal-emulator))")"
- fi
+ # run in selected terminal and account for quirks
+ export TERMINAL_PATH="$(command -v $TERMINAL_NAME)"
+ if [ -x $TERMINAL_PATH ]; then
+ case $TERMINAL_NAME in
+ terminal|termite)
+ # consumes only one argument after -e
+ # options containing space characters are unsupported
+ exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@"
+ ;;
+
+ xterm|konsole|alacritty)
+ # consumes all arguments after -e
+ exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@"
+ ;;
+
+ terminator|xfce4-terminal|mate-terminal)
+ # consumes all arguments after -x
+ exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@"
+ ;;
+
+ gnome-terminal)
+ # consumes all arguments after --
+ exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@"
+ ;;
+
+ kitty)
+ # consumes all trailing arguments
+ exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@"
+ ;;
+
+ *)
+ # If we don't know the terminal, just try to run it in the current shell.
+ # If THAT fails, launch with no output.
+ env TERM=xterm $LAUNCH_FILE "$@"
+ if [ $? -eq 127 ]; then
+ exec $LAUNCH_FILE --no-terminal "$@"
+ fi
+ esac
+
+ ## terminal isn't executable; fallback to current shell or no terminal
+ else
+ echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell."
+ env TERM=xterm $LAUNCH_FILE "$@"
+ if [ $? -eq 127 ]; then
+ exec $LAUNCH_FILE --no-terminal "$@"
+ fi
+ fi
- # run in selected terminal and account for quirks
- export TERMINAL_PATH="$(command -v $TERMINAL_NAME)"
- if [ -x $TERMINAL_PATH ]; then
- case $TERMINAL_NAME in
- terminal|termite)
- # consumes only one argument after -e
- # options containing space characters are unsupported
- exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@"
- ;;
-
- xterm|konsole|alacritty)
- # consumes all arguments after -e
- exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@"
- ;;
-
- terminator|xfce4-terminal|mate-terminal)
- # consumes all arguments after -x
- exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@"
- ;;
-
- gnome-terminal)
- # consumes all arguments after --
- exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@"
- ;;
-
- kitty)
- # consumes all trailing arguments
- exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@"
- ;;
-
- *)
- # If we don't know the terminal, just try to run it in the current shell.
- # If THAT fails, launch with no output.
- env TERM=xterm $LAUNCH_FILE "$@"
- if [ $? -eq 127 ]; then
- exec $LAUNCH_FILE --no-terminal "$@"
- fi
- esac
-
- ## terminal isn't executable; fallback to current shell or no terminal
+ # explicitly run without terminal
else
- echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell."
- env TERM=xterm $LAUNCH_FILE "$@"
- if [ $? -eq 127 ]; then
- exec $LAUNCH_FILE --no-terminal "$@"
- fi
+ exec $LAUNCH_FILE --no-terminal "$@"
fi
fi
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 216a4c32..97e1c243 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.13.2",
+ "Version": "3.13.3",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.13.2"
+ "MinimumApiVersion": "3.13.3"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
index 2a43cb10..0a7ed212 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
@@ -4,11 +4,11 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using Microsoft.Xna.Framework.Content;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.Patching;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
-using SObject = StardewValley.Object;
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
{
@@ -47,6 +47,11 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
original: this.RequireMethod<SaveGame>(nameof(SaveGame.loadDataToLocations)),
prefix: this.GetHarmonyMethod(nameof(SaveGamePatcher.Before_LoadDataToLocations))
);
+
+ harmony.Patch(
+ original: this.RequireMethod<SaveGame>(nameof(SaveGame.LoadFarmType)),
+ finalizer: this.GetHarmonyMethod(nameof(SaveGamePatcher.Finalize_LoadFarmType))
+ );
}
@@ -58,14 +63,35 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <returns>Returns whether to execute the original method.</returns>
private static bool Before_LoadDataToLocations(List<GameLocation> gamelocations)
{
+ // missing locations/NPCs
IDictionary<string, string> npcs = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions");
-
if (SaveGamePatcher.RemoveBrokenContent(gamelocations, npcs))
SaveGamePatcher.OnContentRemoved();
return true;
}
+ /// <summary>The method to call after <see cref="SaveGame.LoadFarmType"/> throws an exception.</summary>
+ /// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
+ /// <returns>Returns the exception to throw, if any.</returns>
+ private static Exception Finalize_LoadFarmType(Exception __exception)
+ {
+ // missing custom farm type
+ if (__exception?.Message?.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _))
+ {
+ SaveGamePatcher.Monitor.Log(__exception.GetLogSummary(), LogLevel.Error);
+ SaveGamePatcher.Monitor.Log($"Removed invalid custom farm type '{SaveGame.loaded.whichFarm}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom farm type mod?)", LogLevel.Warn);
+
+ SaveGame.loaded.whichFarm = Farm.default_layout.ToString();
+ SaveGame.LoadFarmType();
+ SaveGamePatcher.OnContentRemoved();
+
+ __exception = null;
+ }
+
+ return __exception;
+ }
+
/// <summary>Remove content which no longer exists in the game data.</summary>
/// <param name="locations">The current game locations.</param>
/// <param name="npcs">The NPC data.</param>
diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/uk.json b/src/SMAPI.Mods.ErrorHandler/i18n/uk.json
new file mode 100644
index 00000000..a58102ab
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/i18n/uk.json
@@ -0,0 +1,4 @@
+{
+ // warning messages
+ "warn.invalid-content-removed": "Недійсний вміст видалено, щоб запобігти аварійному завершенню роботи (Додаткову інформацію див. на консолі SMAPI)."
+}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index beb52020..c4246721 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.13.2",
+ "Version": "3.13.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.13.2"
+ "MinimumApiVersion": "3.13.3"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 2bd20a63..b1b946c8 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.13.2",
+ "Version": "3.13.3",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.13.2"
+ "MinimumApiVersion": "3.13.3"
}
diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
index 37e4f263..8d4198de 100644
--- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
@@ -157,7 +157,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
#endif
- // default paths
+ // default GOG/Steam paths
foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
{
yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
@@ -165,6 +165,15 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
yield return $@"{programFiles}\GOG Games\Stardew Valley";
yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
}
+
+ // default Xbox app paths
+ // The Xbox app saves the install path to the registry, but we can't use it
+ // here since it saves the internal readonly path (like C:\Program Files\WindowsApps\Mutable\<package ID>)
+ // instead of the mods-enabled path(like C:\Program Files\ModifiableWindowsApps\Stardew Valley).
+ // Fortunately we can cheat a bit: players can customize the install drive, but they can't
+ // change the install path on the drive.
+ for (char driveLetter = 'C'; driveLetter <= 'H'; driveLetter++)
+ yield return $@"{driveLetter}:\Program Files\ModifiableWindowsApps\Stardew Valley";
}
break;
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs
index 39de4b5d..db53d942 100644
--- a/src/SMAPI.Web/Controllers/LogParserController.cs
+++ b/src/SMAPI.Web/Controllers/LogParserController.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using StardewModdingAPI.Toolkit.Utilities;
@@ -39,24 +40,42 @@ namespace StardewModdingAPI.Web.Controllers
***/
/// <summary>Render the log parser UI.</summary>
/// <param name="id">The stored file ID.</param>
- /// <param name="raw">Whether to display the raw unparsed log.</param>
+ /// <param name="format">How to render the log view.</param>
/// <param name="renew">Whether to reset the log expiry.</param>
[HttpGet]
[Route("log")]
[Route("log/{id}")]
- public async Task<ViewResult> Index(string id = null, bool raw = false, bool renew = false)
+ public async Task<ActionResult> Index(string id = null, LogViewFormat format = LogViewFormat.Default, bool renew = false)
{
// fresh page
if (string.IsNullOrWhiteSpace(id))
return this.View("Index", this.GetModel(id));
- // log page
+ // fetch log
StoredFileInfo file = await this.Storage.GetAsync(id, renew);
- ParsedLog log = file.Success
- ? new LogParser().Parse(file.Content)
- : new ParsedLog { IsValid = false, Error = file.Error };
- return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, raw));
+ // render view
+ switch (format)
+ {
+ case LogViewFormat.Default:
+ case LogViewFormat.RawView:
+ {
+ ParsedLog log = file.Success
+ ? new LogParser().Parse(file.Content)
+ : new ParsedLog { IsValid = false, Error = file.Error };
+
+ return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, showRaw: format == LogViewFormat.RawView));
+ }
+
+ case LogViewFormat.RawDownload:
+ {
+ string content = file.Error ?? file.Content;
+ return this.File(Encoding.UTF8.GetBytes(content), "plain/text", $"SMAPI log ({id}).txt");
+ }
+
+ default:
+ throw new InvalidOperationException($"Unknown log view format '{format}'.");
+ }
}
/***
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index 84013ccc..887d0105 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -79,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
// parse log messages
LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "", Loaded = true };
LogModInfo gameMod = new LogModInfo { Name = "game", Author = "", Description = "", Loaded = true };
- IDictionary<string, LogModInfo> mods = new Dictionary<string, LogModInfo>();
+ IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>();
bool inModList = false;
bool inContentPackList = false;
bool inModUpdateList = false;
@@ -99,8 +99,11 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
break;
default:
- if (mods.ContainsKey(message.Mod))
- mods[message.Mod].Errors++;
+ if (mods.TryGetValue(message.Mod, out var entries))
+ {
+ foreach (var entry in entries)
+ entry.Errors++;
+ }
break;
}
}
@@ -127,7 +130,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
string version = match.Groups["version"].Value;
string author = match.Groups["author"].Value;
string description = match.Groups["description"].Value;
- mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description, Loaded = true };
+
+ if (!mods.TryGetValue(name, out List<LogModInfo> entries))
+ mods[name] = entries = new List<LogModInfo>();
+ entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, Loaded = true });
message.Section = LogSection.ModsList;
}
@@ -147,7 +153,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
string author = match.Groups["author"].Value;
string description = match.Groups["description"].Value;
string forMod = match.Groups["for"].Value;
- mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod, Loaded = true };
+
+ if (!mods.TryGetValue(name, out List<LogModInfo> entries))
+ mods[name] = entries = new List<LogModInfo>();
+ entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod, Loaded = true });
message.Section = LogSection.ContentPackList;
}
@@ -165,14 +174,14 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
string name = match.Groups["name"].Value;
string version = match.Groups["version"].Value;
string link = match.Groups["link"].Value;
- if (mods.ContainsKey(name))
- {
- mods[name].UpdateLink = link;
- mods[name].UpdateVersion = version;
- }
- else
+
+ if (mods.TryGetValue(name, out var entries))
{
- mods[name] = new LogModInfo { Name = name, UpdateVersion = version, UpdateLink = link, Loaded = false };
+ foreach (var entry in entries)
+ {
+ entry.UpdateLink = link;
+ entry.UpdateVersion = version;
+ }
}
message.Section = LogSection.ModUpdateList;
@@ -219,7 +228,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
// finalize log
gameMod.Version = log.GameVersion;
- log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray();
+ log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.SelectMany(p => p).OrderBy(p => p.Name)).ToArray();
return log;
}
catch (LogParseException ex)
diff --git a/src/SMAPI.Web/ViewModels/LogViewFormat.cs b/src/SMAPI.Web/ViewModels/LogViewFormat.cs
new file mode 100644
index 00000000..7ef79319
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/LogViewFormat.cs
@@ -0,0 +1,15 @@
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// <summary>How a log file should be displayed.</summary>
+ public enum LogViewFormat
+ {
+ /// <summary>Render a parsed log and metadata.</summary>
+ Default,
+
+ /// <summary>Render a raw log with parsed metadata.</summary>
+ RawView,
+
+ /// <summary>Render directly as a text file.</summary>
+ RawDownload
+ }
+}
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 06d46c9e..91fc3535 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -2,6 +2,7 @@
@using StardewModdingAPI.Toolkit.Utilities
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.Framework.LogParsing.Models
+@using StardewModdingAPI.Web.ViewModels
@model StardewModdingAPI.Web.ViewModels.LogParserModel
@{
@@ -22,13 +23,15 @@
{
<meta name="robots" content="noindex" />
}
- <link rel="stylesheet" href="~/Content/css/file-upload.css?r=202002" />
- <link rel="stylesheet" href="~/Content/css/log-parser.css?r=202002" />
+ <link rel="stylesheet" href="~/Content/css/file-upload.css" />
+ <link rel="stylesheet" href="~/Content/css/log-parser.css" />
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" />
+ <script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
- <script src="~/Content/js/file-upload.js?r=202002"></script>
- <script src="~/Content/js/log-parser.js?r=202002"></script>
+ <script src="~/Content/js/file-upload.js"></script>
+ <script src="~/Content/js/log-parser.js"></script>
<script>
$(function() {
smapi.logParser({
@@ -40,6 +43,8 @@
enableFilters: @this.ForJson(!Model.ShowRaw),
screenIds: @this.ForJson(screenIds)
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
+
+ new Tabby('[data-tabs]');
});
</script>
}
@@ -90,51 +95,65 @@ else if (Model.ParsedLog?.IsValid == true)
@if (Model.ParsedLog == null)
{
<h2>Where do I find my SMAPI log?</h2>
- <div>What system do you use?</div>
- <ul id="os-list">
- @foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows })
- {
- <li>
- <input type="radio" name="os" value="@platform" id="os-@platform" checked="@(Model.DetectedPlatform == platform)" />
- <label for="os-@platform">@platform</label>
- </li>
- }
- </ul>
- <div data-os="@Platform.Android">
- On Android:
- <ol>
- <li>Open a file app (like My Files or MT Manager).</li>
- <li>Find the <code>StardewValley</code> folder on your internal storage.</li>
- <li>Open the <code>ErrorLogs</code> subfolder.</li>
- <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
- </ol>
- </div>
- <div data-os="@Platform.Linux">
- On Linux:
- <ol>
- <li>Open the Files app.</li>
- <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li>
- <li>Choose <em>Enter Location</em>.</li>
- <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
- <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
- </ol>
- </div>
- <div data-os="@Platform.Mac">
- On macOS:
- <ol>
- <li>Open the Finder app.</li>
- <li>Click <em>Go</em> at the top, then <em>Go to Folder</em>.</li>
- <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
- <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
- </ol>
- </div>
- <div data-os="@Platform.Windows">
- On Windows:
- <ol>
- <li>Press the <code>Windows</code> and <code>R</code> buttons at the same time.</li>
- <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li>
- <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
- </ol>
+ <div id="os-instructions">
+ <div>
+ <ul data-tabs>
+ @foreach (Platform platform in new[] {Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows})
+ {
+ @if (platform == Platform.Windows)
+ {
+ <li><a data-tabby-default href="#@(platform)-steamgog">@platform (Steam or GOG)</a></li>
+ <li><a href="#@(platform)-xbox">@platform (Xbox app)</a></li>
+ }
+ else
+ {
+ <li><a href="#@platform">@platform</a></li>
+ }
+ }
+ </ul>
+ </div>
+ <div>
+ <div id="@Platform.Android">
+ <ol>
+ <li>Open a file app (like My Files or MT Manager).</li>
+ <li>Find the <code>StardewValley</code> folder on your internal storage.</li>
+ <li>Open the <code>ErrorLogs</code> subfolder.</li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ <div id="@Platform.Linux">
+ <ol>
+ <li>Open the Files app.</li>
+ <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li>
+ <li>Choose <em>Enter Location</em>.</li>
+ <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ <div id="@Platform.Mac">
+ <ol>
+ <li>Open the Finder app.</li>
+ <li>Click <em>Go</em> at the top, then <em>Go to Folder</em>.</li>
+ <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ <div id="@(Platform.Windows)-steamgog">
+ <ol>
+ <li>Press the <kbd>Windows</kbd> and <kbd>R</kbd> buttons at the same time.</li>
+ <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ <div id="@(Platform.Windows)-xbox">
+ <ol>
+ <li>Press the <kbd>Windows</kbd> and <kbd>R</kbd> buttons at the same time.</li>
+ <li>In the 'run' box that appears, enter this exact text: <pre>%localappdata%\Packages\ConcernedApe.StardewValleyPC_0c8vynj4cqe4e\LocalCache\Roaming\StardewValley\ErrorLogs</pre></li>
+ <li>If you get an error with the title "Location is not available", try the "with Steam or GOG" instructions above.</li>
+ <li>Otherwise the log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ </div>
</div>
<h2>How do I share my log?</h2>
@@ -338,14 +357,24 @@ else if (Model.ParsedLog?.IsValid == true)
}
}
</table>
-
- <small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, raw = true })">view raw log</a></small>
}
else
{
<pre v-pre>@Model.ParsedLog.RawText</pre>
- <small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })">view parsed log</a></small>
}
+
+ <small>
+ @if (Model.ShowRaw)
+ {
+ <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })">view parsed log</a>
+ }
+ else
+ {
+ <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawView })">view raw log</a>
+ }
+
+ | <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawDownload })" download>download</a>
+ </small>
</div>
}
else if (Model.ParsedLog?.IsValid == false)
diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
index bfbc8982..8c3acceb 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
@@ -291,13 +291,29 @@ table caption {
/*********
-** Upload form
+** OS instructions
*********/
-#os-list {
- list-style: none;
+#os-instructions {
+ display: grid;
+ grid-template-columns: minmax(16em, auto) 30em;
}
-div[data-os] {
- display: none;
+#os-instructions [role="tablist"] {
+ border: 0;
+}
+
+#os-instructions [role="tab"] {
+ display: block;
+ border: 0;
+ position: relative;
}
+#os-instructions [role="tab"][aria-selected="true"] {
+ font-weight: bold;
+ border-radius: 0 10px 10px 0;
+}
+
+[role="tab"][aria-selected="true"]::after {
+ content: "▶";
+ padding-left: 0.5em;
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
index 6ae1707e..90715375 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
@@ -115,17 +115,6 @@ smapi.logParser = function (data, sectionUrl) {
*********/
var input = $("#input");
if (input.length) {
- // instructions per OS
- var systemOptions = $("input[name='os']");
- var systemInstructions = $("div[data-os]");
-
- var chooseSystem = function () {
- systemInstructions.hide();
- systemInstructions.filter("[data-os='" + $("input[name='os']:checked").val() + "']").show();
- };
- systemOptions.on("click", chooseSystem);
- chooseSystem();
-
// file upload
smapi.fileUpload({
chooseFileLink: $("#choose-file-link"),
diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
index eb76b29a..75a3f8c7 100644
--- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
@@ -481,6 +481,13 @@
"~1.2.2 | StatusReasonDetails": "references the deleted Content/Mine asset"
},
+ "Critical Crow": {
+ "ID": "leonary.CRCROWS",
+ "Default | UpdateKey": "Nexus:2663",
+ "~1.2.2 | Status": "AssumeBroken",
+ "~1.2.2 | StatusReasonDetails": "removes newer content from the TileSheets/Craftables asset"
+ },
+
"Green Pastures Farm": {
"ID": "bugbuddy.GreenPasturesFarm",
"Default | UpdateKey": "Nexus:2326",
diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json
index 05698ba2..b6722347 100644
--- a/src/SMAPI.Web/wwwroot/schemas/manifest.json
+++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json
@@ -103,7 +103,7 @@
"type": "array",
"items": {
"type": "string",
- "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)$",
+ "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)(?: *@ *[a-zA-Z0-9_]+ *)$",
"@errorMessages": {
"pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info."
}
diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings
index 9a6cad37..71cd7b82 100644
--- a/src/SMAPI.sln.DotSettings
+++ b/src/SMAPI.sln.DotSettings
@@ -70,6 +70,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=thumbstick/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheet/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheets/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheet_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unloadable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=virally/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary> \ No newline at end of file
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 5de28f84..455cfd7e 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -49,7 +49,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.13.2";
+ internal static string RawApiVersion = "3.13.3";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -65,7 +65,7 @@ namespace StardewModdingAPI
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion);
/// <summary>The minimum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.5");
+ public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6");
/// <summary>The maximum supported version of Stardew Valley.</summary>
public static ISemanticVersion MaximumGameVersion { get; } = null;
@@ -340,5 +340,16 @@ namespace StardewModdingAPI
// if save doesn't exist yet, return the default one we expect to be created
return folder;
}
+
+ /// <summary>Get a display label for the game's build number.</summary>
+ internal static string GetBuildVersionLabel()
+ {
+ string version = typeof(Game1).Assembly.GetName().Version?.ToString() ?? "unknown";
+
+ if (version.StartsWith($"{Game1.version}."))
+ version = version.Substring(Game1.version.Length + 1);
+
+ return version;
+ }
}
}
diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs
index 2ea38430..0919bb44 100644
--- a/src/SMAPI/Framework/Content/TilesheetReference.cs
+++ b/src/SMAPI/Framework/Content/TilesheetReference.cs
@@ -1,3 +1,6 @@
+using System.Numerics;
+using xTile.Dimensions;
+
namespace StardewModdingAPI.Framework.Content
{
/// <summary>Basic metadata about a vanilla tilesheet.</summary>
@@ -15,6 +18,12 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>The asset path for the tilesheet texture.</summary>
public readonly string ImageSource;
+ /// <summary>The number of tiles in the tilesheet.</summary>
+ public readonly Size SheetSize;
+
+ /// <summary>The size of each tile in pixels.</summary>
+ public readonly Size TileSize;
+
/*********
** Public methods
@@ -23,11 +32,15 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="index">The tilesheet's index in the list.</param>
/// <param name="id">The tilesheet's unique ID in the map.</param>
/// <param name="imageSource">The asset path for the tilesheet texture.</param>
- public TilesheetReference(int index, string id, string imageSource)
+ /// <param name="sheetSize">The number of tiles in the tilesheet.</param>
+ /// <param name="tileSize">The size of each tile in pixels.</param>
+ public TilesheetReference(int index, string id, string imageSource, Size sheetSize, Size tileSize)
{
this.Index = index;
this.Id = id;
this.ImageSource = imageSource;
+ this.SheetSize = sheetSize;
+ this.TileSize = tileSize;
}
}
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index b6f1669a..99091f3e 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -406,14 +406,14 @@ namespace StardewModdingAPI.Framework
if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets))
{
tilesheets = this.TryLoadVanillaAsset(assetName, out Map map)
- ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource)).ToArray()
+ ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource, sheet.SheetSize, sheet.TileSize)).ToArray()
: null;
this.VanillaTilesheets[assetName] = tilesheets;
this.VanillaContentManager.Unload();
}
- return tilesheets ?? new TilesheetReference[0];
+ return tilesheets ?? Array.Empty<TilesheetReference>();
}
/// <summary>Get the language enum which corresponds to a locale code (e.g. <see cref="LocalizedContentManager.LanguageCode.fr"/> given <c>fr-FR</c>).</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 7a49dd36..ab198076 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -13,6 +13,7 @@ using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Internal;
using StardewValley;
using xTile;
+using xTile.Tiles;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -308,7 +309,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// return matched asset
- return this.TryValidateLoadedAsset(info, data, mod)
+ return this.TryFixAndValidateLoadedAsset(info, data, mod)
? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName)
: null;
}
@@ -381,12 +382,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
return asset;
}
- /// <summary>Validate that an asset loaded by a mod is valid and won't cause issues.</summary>
+ /// <summary>Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
/// <param name="data">The loaded asset data.</param>
/// <param name="mod">The mod which loaded the asset.</param>
- private bool TryValidateLoadedAsset<T>(IAssetInfo info, T data, IModMetadata mod)
+ /// <returns>Returns whether the asset passed validation checks (after any fixes were applied).</returns>
+ private bool TryFixAndValidateLoadedAsset<T>(IAssetInfo info, T data, IModMetadata mod)
{
// can't load a null asset
if (data == null)
@@ -401,20 +403,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName);
foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs)
{
- // skip if match
- if (loadedMap.TileSheets.Count > vanillaSheet.Index && loadedMap.TileSheets[vanillaSheet.Index].Id == vanillaSheet.Id)
- continue;
+ // add missing tilesheet
+ if (loadedMap.GetTileSheet(vanillaSheet.Id) == null)
+ {
+ mod.Monitor.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn);
+ this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.AssetName}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource}).");
+
+ loadedMap.AddTileSheet(new TileSheet(vanillaSheet.Id, loadedMap, vanillaSheet.ImageSource, vanillaSheet.SheetSize, vanillaSheet.TileSize));
+ }
// handle mismatch
+ if (loadedMap.TileSheets.Count <= vanillaSheet.Index || loadedMap.TileSheets[vanillaSheet.Index].Id != vanillaSheet.Id)
{
// only show warning if not farm map
// This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting.
bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining");
- int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id);
- string reason = loadedIndex != -1
- ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."
- : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes.";
+ string reason = $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help.";
SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval);
if (isFarmMap)
@@ -429,19 +434,5 @@ namespace StardewModdingAPI.Framework.ContentManagers
return true;
}
-
- /// <summary>Find a map tilesheet by ID.</summary>
- /// <param name="map">The map whose tilesheets to search.</param>
- /// <param name="id">The tilesheet ID to match.</param>
- private int TryFindTilesheet(Map map, string id)
- {
- for (int i = 0; i < map.TileSheets.Count; i++)
- {
- if (map.TileSheets[i].Id == id)
- return i;
- }
-
- return -1;
- }
}
}
diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
index d99f1dd2..bad69a2a 100644
--- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
+++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
@@ -8,15 +8,11 @@ namespace StardewModdingAPI.Framework.Logging
internal class InterceptingTextWriter : TextWriter
{
/*********
- ** Fields
+ ** Accessors
*********/
/// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
- private readonly char IgnoreChar;
-
+ public const char IgnoreChar = '\u200B';
- /*********
- ** Accessors
- *********/
/// <summary>The underlying console output.</summary>
public TextWriter Out { get; }
@@ -26,30 +22,48 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>The event raised when a message is written to the console directly.</summary>
public event Action<string> OnMessageIntercepted;
+ /// <summary>Whether the text writer should ignore the next input if it's a newline.</summary>
+ /// <remarks>This is used when log output is suppressed from the console, since <c>Console.WriteLine</c> writes the trailing newline as a separate call.</remarks>
+ public bool IgnoreNextIfNewline { get; set; }
+
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="output">The underlying output writer.</param>
- /// <param name="ignoreChar">Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</param>
- public InterceptingTextWriter(TextWriter output, char ignoreChar)
+ public InterceptingTextWriter(TextWriter output)
{
this.Out = output;
- this.IgnoreChar = ignoreChar;
}
/// <inheritdoc />
public override void Write(char[] buffer, int index, int count)
{
- if (buffer.Length == 0)
+ // track newline skip
+ bool ignoreIfNewline = this.IgnoreNextIfNewline;
+ this.IgnoreNextIfNewline = false;
+
+ // get first character if valid
+ if (count == 0 || index < 0 || index >= buffer.Length)
+ {
this.Out.Write(buffer, index, count);
- else if (buffer[0] == this.IgnoreChar)
+ return;
+ }
+ char firstChar = buffer[index];
+
+ // handle output
+ if (firstChar == InterceptingTextWriter.IgnoreChar)
this.Out.Write(buffer, index + 1, count - 1);
- else if (this.IsEmptyOrNewline(buffer))
+ else if (char.IsControl(firstChar) && firstChar is not ('\r' or '\n'))
this.Out.Write(buffer, index, count);
+ else if (this.IsEmptyOrNewline(buffer))
+ {
+ if (!ignoreIfNewline)
+ this.Out.Write(buffer, index, count);
+ }
else
- this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n'));
+ this.OnMessageIntercepted?.Invoke(new string(buffer, index, count));
}
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 5a291d0a..a8a8b6ee 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -25,8 +25,11 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>The log file to which to write messages.</summary>
private readonly LogFileManager LogFile;
+ /// <summary>The text writer which intercepts console output.</summary>
+ private readonly InterceptingTextWriter ConsoleInterceptor;
+
/// <summary>Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
- private readonly char IgnoreChar = '\u200B';
+ private const char IgnoreChar = InterceptingTextWriter.IgnoreChar;
/// <summary>Get a named monitor instance.</summary>
private readonly Func<string, Monitor> GetMonitorImpl;
@@ -34,22 +37,22 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>Regex patterns which match console non-error messages to suppress from the console and log.</summary>
private readonly Regex[] SuppressConsolePatterns =
{
- new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^DebugOutput:\s+(?:added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^Ignoring keys: ", RegexOptions.Compiled | RegexOptions.CultureInvariant)
+ new(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^DebugOutput:\s+(?:added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^Ignoring keys: ", RegexOptions.Compiled | RegexOptions.CultureInvariant)
};
/// <summary>Regex patterns which match console messages to show a more friendly error for.</summary>
private readonly ReplaceLogPattern[] ReplaceConsolePatterns =
{
// Steam not loaded
- new ReplaceLogPattern(
+ new(
search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
replacement:
#if SMAPI_FOR_WINDOWS
- "Oops! Steam achievements won't work because Steam isn't loaded. See 'Launch SMAPI through Steam or GOG Galaxy' in the install guide for more info: https://smapi.io/install.",
+ "Oops! Steam achievements won't work because Steam isn't loaded. See 'Configure your game client' in the install guide for more info: https://smapi.io/install.",
#else
"Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.",
#endif
@@ -57,7 +60,7 @@ namespace StardewModdingAPI.Framework.Logging
),
// save file not found error
- new ReplaceLogPattern(
+ new(
search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.",
logLevel: LogLevel.Error
@@ -91,7 +94,7 @@ namespace StardewModdingAPI.Framework.Logging
public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode, Func<int?> getScreenIdForLog)
{
// init construction logic
- this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
+ this.GetMonitorImpl = name => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
{
WriteToConsole = writeToConsole,
ShowTraceInConsole = isDeveloperMode,
@@ -104,10 +107,10 @@ namespace StardewModdingAPI.Framework.Logging
this.MonitorForGame = this.GetMonitor("game");
// redirect direct console output
- var output = new InterceptingTextWriter(Console.Out, this.IgnoreChar);
+ this.ConsoleInterceptor = new InterceptingTextWriter(Console.Out);
if (writeToConsole)
- output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
- Console.SetOut(output);
+ this.ConsoleInterceptor.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
+ Console.SetOut(this.ConsoleInterceptor);
// enable Unicode handling on Windows
// (the terminal defaults to UTF-8 on Linux/macOS)
@@ -146,7 +149,7 @@ namespace StardewModdingAPI.Framework.Logging
.Add(new ReloadI18nCommand(reloadTranslations), this.Monitor);
// start handling command line input
- Thread inputThread = new Thread(() =>
+ Thread inputThread = new(() =>
{
while (true)
{
@@ -262,7 +265,7 @@ namespace StardewModdingAPI.Framework.Logging
public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
{
// log platform
- this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
+ this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
// log basic info
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
@@ -363,7 +366,10 @@ namespace StardewModdingAPI.Framework.Logging
// ignore suppressed message
if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message)))
+ {
+ this.ConsoleInterceptor.IgnoreNextIfNewline = true;
return;
+ }
// show friendly error if applicable
foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns)
@@ -383,6 +389,7 @@ namespace StardewModdingAPI.Framework.Logging
// forward to monitor
gameMonitor.Log(message, level);
+ this.ConsoleInterceptor.IgnoreNextIfNewline = true;
}
/// <summary>Write a summary of mod warnings to the console and log.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
index 60bbd2c7..d7cb2471 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
@@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
** Fields
*********/
/// <summary>The comparer which heuristically compares type definitions.</summary>
- private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer();
+ private static readonly TypeReferenceComparer TypeDefinitionComparer = new();
/*********
@@ -28,28 +28,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
: null;
}
- /// <summary>Get whether the field is a reference to the expected type and field.</summary>
- /// <param name="instruction">The IL instruction.</param>
- /// <param name="fullTypeName">The full type name containing the expected field.</param>
- /// <param name="fieldName">The name of the expected field.</param>
- public static bool IsFieldReferenceTo(Instruction instruction, string fullTypeName, string fieldName)
- {
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
- return RewriteHelper.IsFieldReferenceTo(fieldRef, fullTypeName, fieldName);
- }
-
- /// <summary>Get whether the field is a reference to the expected type and field.</summary>
- /// <param name="fieldRef">The field reference to check.</param>
- /// <param name="fullTypeName">The full type name containing the expected field.</param>
- /// <param name="fieldName">The name of the expected field.</param>
- public static bool IsFieldReferenceTo(FieldReference fieldRef, string fullTypeName, string fieldName)
- {
- return
- fieldRef != null
- && fieldRef.DeclaringType.FullName == fullTypeName
- && fieldRef.Name == fieldName;
- }
-
/// <summary>Get the method reference from an instruction if it matches.</summary>
/// <param name="instruction">The IL instruction.</param>
public static MethodReference AsMethodReference(Instruction instruction)
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
index 0b679e9d..857a2230 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -12,54 +13,55 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/*********
** Fields
*********/
- /// <summary>The type containing the field to which references should be rewritten.</summary>
- private readonly Type Type;
-
- /// <summary>The field name to which references should be rewritten.</summary>
- private readonly string FromFieldName;
-
- /// <summary>The new field to reference.</summary>
- private readonly FieldInfo ToField;
+ /// <summary>The new fields to reference indexed by the old field/type names.</summary>
+ private readonly Dictionary<string, Dictionary<string, FieldInfo>> FieldMaps = new();
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ public FieldReplaceRewriter()
+ : base(defaultPhrase: "field replacement") { } // will be overridden when a field is replaced
+
+ /// <summary>Add a field to replace.</summary>
/// <param name="fromType">The type whose field to rewrite.</param>
/// <param name="fromFieldName">The field name to rewrite.</param>
/// <param name="toType">The new type which will have the field.</param>
/// <param name="toFieldName">The new field name to reference.</param>
- public FieldReplaceRewriter(Type fromType, string fromFieldName, Type toType, string toFieldName)
- : base(defaultPhrase: $"{fromType.FullName}.{fromFieldName} field")
+ public FieldReplaceRewriter AddField(Type fromType, string fromFieldName, Type toType, string toFieldName)
{
- this.Type = fromType;
- this.FromFieldName = fromFieldName;
- this.ToField = toType.GetField(toFieldName);
- if (this.ToField == null)
+ // get full type name
+ string fromTypeName = fromType?.FullName;
+ if (fromTypeName == null)
+ throw new InvalidOperationException($"Can't replace field for invalid type reference {toType}.");
+
+ // get target field
+ FieldInfo toField = toType.GetField(toFieldName);
+ if (toField == null)
throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field.");
- }
- /// <summary>Construct an instance.</summary>
- /// <param name="type">The type whose field to rewrite.</param>
- /// <param name="fromFieldName">The field name to rewrite.</param>
- /// <param name="toFieldName">The new field name to reference.</param>
- public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName)
- : this(type, fromFieldName, type, toFieldName)
- {
+ // add mapping
+ if (!this.FieldMaps.TryGetValue(fromTypeName, out var fieldMap))
+ this.FieldMaps[fromTypeName] = fieldMap = new();
+ fieldMap[fromFieldName] = toField;
+
+ return this;
}
/// <inheritdoc />
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
- // get field reference
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
- if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName))
+ string declaringType = fieldRef?.DeclaringType?.FullName;
+
+ // get mapped field
+ if (declaringType == null || !this.FieldMaps.TryGetValue(declaringType, out var fieldMap) || !fieldMap.TryGetValue(fieldRef.Name, out FieldInfo toField))
return false;
// replace with new field
- instruction.Operand = module.ImportReference(this.ToField);
-
+ this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} field");
+ instruction.Operand = module.ImportReference(toField);
return this.MarkRewritten();
}
}
diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index 04e67d68..ab76e7c0 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -28,7 +28,7 @@ namespace StardewModdingAPI.Framework
private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max();
/// <summary>A cache of messages that should only be logged once.</summary>
- private readonly HashSet<string> LogOnceCache = new HashSet<string>();
+ private readonly HashSet<string> LogOnceCache = new();
/// <summary>Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</summary>
private readonly Func<int?> GetScreenIdForLog;
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 232e54ce..367372b2 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -37,9 +37,10 @@ namespace StardewModdingAPI.Metadata
if (rewriteMods)
{
// rewrite for Stardew Valley 1.5
- yield return new FieldReplaceRewriter(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture));
- yield return new FieldReplaceRewriter(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
- yield return new FieldReplaceRewriter(typeof(MineShaft), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
+ yield return new FieldReplaceRewriter()
+ .AddField(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture))
+ .AddField(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps))
+ .AddField(typeof(MineShaft), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
// heuristic rewrites
yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies);
@@ -87,7 +88,7 @@ namespace StardewModdingAPI.Metadata
typeof(System.IO.DirectoryInfo).FullName,
typeof(System.IO.DriveInfo).FullName,
typeof(System.IO.FileSystemWatcher).FullName
- },
+ },
InstructionHandleResult.DetectedFilesystemAccess
);
diff --git a/src/SMAPI/i18n/uk.json b/src/SMAPI/i18n/uk.json
new file mode 100644
index 00000000..d84aabcf
--- /dev/null
+++ b/src/SMAPI/i18n/uk.json
@@ -0,0 +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": "{{season}} День {{day}}, Рік {{year}}"
+}