summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-03-21 16:38:23 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-03-21 16:38:23 -0400
commit75f3600ab1eae06463ae8f386c5ab71f3815142f (patch)
tree5bff95c0446b9e70a06c1525fa7387b21ff148cc
parentfc5fc54ab1c375e20b3e4f947bb11f08b4983bd1 (diff)
parent74215e844ae2af0075e5df3ab6a5f58efff4f981 (diff)
downloadSMAPI-75f3600ab1eae06463ae8f386c5ab71f3815142f.tar.gz
SMAPI-75f3600ab1eae06463ae8f386c5ab71f3815142f.tar.bz2
SMAPI-75f3600ab1eae06463ae8f386c5ab71f3815142f.zip
Merge branch 'develop' into stable
-rw-r--r--build/common.targets4
-rw-r--r--docs/release-notes.md21
-rw-r--r--docs/technical/smapi.md4
-rw-r--r--src/SMAPI.Installer/SMAPI.Installer.csproj1
-rw-r--r--src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj1
-rw-r--r--src/SMAPI.ModBuildConfig/build/smapi.targets26
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs95
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj16
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs10
-rw-r--r--src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj16
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Tests/SMAPI.Tests.csproj7
-rw-r--r--src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj1
-rw-r--r--src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs7
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj3
-rw-r--r--src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs7
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj6
-rw-r--r--src/SMAPI/Constants.cs97
-rw-r--r--src/SMAPI/Context.cs3
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs69
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs3
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs25
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs4
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs11
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs7
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs5
-rw-r--r--src/SMAPI/Framework/SCore.cs7
-rw-r--r--src/SMAPI/GameFramework.cs12
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs175
-rw-r--r--src/SMAPI/SMAPI.config.json5
-rw-r--r--src/SMAPI/SMAPI.csproj17
-rw-r--r--src/SMAPI/Utilities/KeybindList.cs5
37 files changed, 469 insertions, 221 deletions
diff --git a/build/common.targets b/build/common.targets
index d9d21466..d680fa74 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -4,12 +4,12 @@
<!--set properties -->
<PropertyGroup>
- <Version>3.9.4</Version>
+ <Version>3.9.5</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
- <DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
+ <DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS;SMAPI_FOR_XNA</DefineConstants>
</PropertyGroup>
<!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors -->
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 045a5168..ad644532 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -7,6 +7,27 @@
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
-->
+## 3.9.5
+Released 21 March 2021 for Stardew Valley 1.5.4 or later.
+
+* For players:
+ * Added console command to reset community center bundles _(in Console Commands)_.
+ * Disabled aggressive memory optimization by default.
+ _The option was added in SMAPI 3.9.2 to reduce errors for some players, but it can cause multiplayer crashes with some mods. If you often see `OutOfMemoryException` errors, you can edit `smapi-internal/config.json` to re-enable it. We're experimenting with making Stardew Valley 64-bit to address memory issues more systematically._
+ * Fixed bundles corrupted in non-English saves created after SMAPI 3.9.2.
+ _If you have an affected save, you can load your save and then enter the `regenerate_bundles confirm` command in the SMAPI console to fix it._
+ * Internal changes to prepare for unofficial 64-bit.
+
+* For mod authors:
+ * Improved asset propagation:
+ * Added for interior door sprites.
+ * SMAPI now updates the NPC pathfinding cache when map warps are changed through the content API.
+ * Reduced performance impact of invalidating cached assets before a save is loaded.
+ * Fixed asset changes not reapplied in the edge case where you're playing in non-English, and the changes are only applied after the save is loaded, and the player returns to title and reloads a save, and the game reloads the target asset before the save is loaded.
+ * Added a second `KeybindList` constructor to simplify single-key default bindings.
+ * Added a `Constants.GameFramework` field which indicates whether the game is using XNA Framework or MonoGame.
+ _Note: mods don't need to handle the difference in most cases, but some players may use MonoGame on Windows in upcoming versions. Mods which check `Constants.TargetPlatform` should review usages as needed._
+
## 3.9.4
Released 07 March 2021 for Stardew Valley 1.5.4 or later.
diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md
index e2832710..e77d9d82 100644
--- a/docs/technical/smapi.md
+++ b/docs/technical/smapi.md
@@ -50,14 +50,14 @@ environment variable | purpose
`SMAPI_NO_TERMINAL` | Equivalent to `--no-terminal` above.
`SMAPI_MODS_PATH` | Equivalent to `--mods-path` above.
-
### Compile flags
SMAPI uses a small number of conditional compilation constants, which you can set by editing the
`<DefineConstants>` element in `SMAPI.csproj`. Supported constants:
flag | purpose
---- | -------
-`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
+`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled for Windows; if not set, the code assumes Linux/MacOS. Set automatically in `common.targets`.
+`SMAPI_FOR_XNA` | Whether SMAPI is being compiled for XNA Framework; if not set, the code assumes MonoGame. Set automatically in `common.targets` with the same value as `SMAPI_FOR_WINDOWS`.
`HARMONY_2` | Whether to enable experimental Harmony 2.0 support and rewrite existing Harmony 1._x_ mods for compatibility. Note that you need to replace `build/0Harmony.dll` with a Harmony 2.0 build (or switch to a package reference) to use this flag.
## For SMAPI developers
diff --git a/src/SMAPI.Installer/SMAPI.Installer.csproj b/src/SMAPI.Installer/SMAPI.Installer.csproj
index 44ed3bd1..1777be5f 100644
--- a/src/SMAPI.Installer/SMAPI.Installer.csproj
+++ b/src/SMAPI.Installer/SMAPI.Installer.csproj
@@ -4,7 +4,6 @@
<Description>The SMAPI installer for players.</Description>
<TargetFramework>net45</TargetFramework>
<OutputType>Exe</OutputType>
- <PlatformTarget>x86</PlatformTarget>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
index 1813f58b..5992fbbf 100644
--- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
+++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
@@ -3,7 +3,6 @@
<!--build-->
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
<TargetFramework>net45</TargetFramework>
- <PlatformTarget>x86</PlatformTarget>
<LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets
index 65544b12..76a1536c 100644
--- a/src/SMAPI.ModBuildConfig/build/smapi.targets
+++ b/src/SMAPI.ModBuildConfig/build/smapi.targets
@@ -47,19 +47,27 @@
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
- <!-- Windows -->
+ <!-- Windows only -->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
- <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
- <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
- <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
- <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
- <!-- Linux/Mac -->
- <ItemGroup Condition="'$(OS)' != 'Windows_NT'">
- <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" />
- </ItemGroup>
+ <!-- Game framework -->
+ <Choose>
+ <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
+ <ItemGroup>
+ <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
+ <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
+ <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
+ <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
+ </ItemGroup>
+ </When>
+ <Otherwise>
+ <ItemGroup>
+ <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" />
+ </ItemGroup>
+ </Otherwise>
+ </Choose>
<!--*********************************************
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs
new file mode 100644
index 00000000..9beedb96
--- /dev/null
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Netcode;
+using StardewValley;
+using StardewValley.Network;
+
+namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
+{
+ /// <summary>A command which regenerates the game's bundles.</summary>
+ internal class RegenerateBundlesCommand : ConsoleCommand
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public RegenerateBundlesCommand()
+ : base("regenerate_bundles", $"Regenerate the game's community center bundle data. WARNING: this will reset all bundle progress, and may have unintended effects if you've already completed bundles. DO NOT USE THIS unless you're absolutely sure.\n\nUsage: regenerate_bundles confirm [<type>] [ignore_seed]\nRegenerate all bundles for this save. If the <type> is set to '{string.Join("' or '", Enum.GetNames(typeof(Game1.BundleType)))}', change the bundle type for the save. If an 'ignore_seed' option is included, remixed bundles are re-randomized without using the predetermined save seed.\n\nExample: regenerate_bundles remixed confirm") { }
+
+ /// <summary>Handle the command.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="command">The command name.</param>
+ /// <param name="args">The command arguments.</param>
+ public override void Handle(IMonitor monitor, string command, ArgumentParser args)
+ {
+ // get flags
+ var bundleType = Game1.bundleType;
+ bool confirmed = false;
+ bool useSeed = true;
+ foreach (string arg in args)
+ {
+ if (arg.Equals("confirm", StringComparison.OrdinalIgnoreCase))
+ confirmed = true;
+ else if (arg.Equals("ignore_seed", StringComparison.OrdinalIgnoreCase))
+ useSeed = false;
+ else if (Enum.TryParse(arg, ignoreCase: true, out Game1.BundleType type))
+ bundleType = type;
+ else
+ {
+ monitor.Log($"Invalid option '{arg}'. Type 'help {command}' for usage.", LogLevel.Error);
+ return;
+ }
+ }
+
+ // require confirmation
+ if (!confirmed)
+ {
+ monitor.Log($"WARNING: this may have unintended consequences (type 'help {command}' for details). Are you sure?", LogLevel.Warn);
+
+ string[] newArgs = args.Concat(new[] { "confirm" }).ToArray();
+ monitor.Log($"To confirm, enter this command: '{command} {string.Join(" ", newArgs)}'.", LogLevel.Info);
+ return;
+ }
+
+ // need a loaded save
+ if (!Context.IsWorldReady)
+ {
+ monitor.Log("You need to load a save to use this command.", LogLevel.Error);
+ return;
+ }
+
+ // get private fields
+ IWorldState state = Game1.netWorldState.Value;
+ var bundleData = state.GetType().GetField("_bundleData", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)?.GetValue(state) as IDictionary<string, string>
+ ?? throw new InvalidOperationException("Can't access '_bundleData' field on world state.");
+ var netBundleData = state.GetType().GetField("netBundleData", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)?.GetValue(state) as NetStringDictionary<string, NetString>
+ ?? throw new InvalidOperationException("Can't access 'netBundleData' field on world state.");
+
+ // clear bundle data
+ state.BundleData.Clear();
+ state.Bundles.Clear();
+ state.BundleRewards.Clear();
+ bundleData.Clear();
+ netBundleData.Clear();
+
+ // regenerate bundles
+ var locale = LocalizedContentManager.CurrentLanguageCode;
+ try
+ {
+ LocalizedContentManager.CurrentLanguageCode = LocalizedContentManager.LanguageCode.en; // the base bundle data needs to be unlocalized (the game will add localized names later)
+
+ Game1.bundleType = bundleType;
+ Game1.GenerateBundles(bundleType, use_seed: useSeed);
+ }
+ finally
+ {
+ LocalizedContentManager.CurrentLanguageCode = locale;
+ }
+
+ monitor.Log("Regenerated bundles and reset bundle progress.", LogLevel.Info);
+ monitor.Log("This may have unintended effects if you've already completed any bundles. If you're not sure, exit your game without saving to cancel.", LogLevel.Warn);
+ }
+ }
+}
diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj
index 1e3208de..a187c1ff 100644
--- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj
+++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj
@@ -4,9 +4,10 @@
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
<TargetFramework>net45</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
- <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
</ItemGroup>
@@ -16,19 +17,21 @@
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" />
</ItemGroup>
+ <!-- Windows only -->
+ <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+ <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
+ </ItemGroup>
+
+ <!-- Game framework -->
<Choose>
- <!-- Windows -->
- <When Condition="$(OS) == 'Windows_NT'">
+ <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
<ItemGroup>
- <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
</ItemGroup>
</When>
-
- <!-- Linux/Mac -->
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
@@ -41,5 +44,4 @@
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
- <Import Project="..\..\build\common.targets" />
</Project>
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 2811a11c..65c66d33 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.9.4",
+ "Version": "3.9.5",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.9.4"
+ "MinimumApiVersion": "3.9.5"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
index 95e4f5ef..8056fd71 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
@@ -27,11 +27,9 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
#endif
{
harmony.Patch(
-#if SMAPI_FOR_WINDOWS
- original: AccessTools.Method(typeof(SpriteBatch), "InternalDraw"),
-#else
- original: AccessTools.Method(typeof(SpriteBatch), "CheckValid", new[] { typeof(Texture2D) }),
-#endif
+ original: Constants.GameFramework == GameFramework.Xna
+ ? AccessTools.Method(typeof(SpriteBatch), "InternalDraw")
+ : AccessTools.Method(typeof(SpriteBatch), "CheckValid", new[] { typeof(Texture2D) }),
postfix: new HarmonyMethod(this.GetType(), nameof(SpriteBatchValidationPatches.After_SpriteBatch_CheckValid))
);
}
@@ -40,7 +38,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/*********
** Private methods
*********/
-#if SMAPI_FOR_WINDOWS
+#if SMAPI_FOR_XNA
/// <summary>The method to call instead of <see cref="SpriteBatch.InternalDraw"/>.</summary>
/// <param name="texture">The texture to validate.</param>
#else
diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj
index 5c0cf952..788f6f16 100644
--- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj
+++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj
@@ -4,9 +4,10 @@
<RootNamespace>StardewModdingAPI.Mods.ErrorHandler</RootNamespace>
<TargetFramework>net45</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
- <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
<Reference Include="..\..\build\0Harmony.dll" Private="False" />
@@ -16,19 +17,21 @@
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
</ItemGroup>
+ <!-- Windows only -->
+ <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+ <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
+ </ItemGroup>
+
+ <!-- Game framework -->
<Choose>
- <!-- Windows -->
- <When Condition="$(OS) == 'Windows_NT'">
+ <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
<ItemGroup>
- <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
</ItemGroup>
</When>
-
- <!-- Linux/Mac -->
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
@@ -42,5 +45,4 @@
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
- <Import Project="..\..\build\common.targets" />
</Project>
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index 52bf4f6a..1e810113 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.9.4",
+ "Version": "3.9.5",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.9.4"
+ "MinimumApiVersion": "3.9.5"
}
diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj
index 98a3f0cc..a6f76781 100644
--- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj
+++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj
@@ -4,9 +4,10 @@
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
<TargetFramework>net45</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
- <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
</ItemGroup>
@@ -20,5 +21,4 @@
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
- <Import Project="..\..\build\common.targets" />
</Project>
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index b88582ba..ced7888a 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.9.4",
+ "Version": "3.9.5",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.9.4"
+ "MinimumApiVersion": "3.9.5"
}
diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj
index 51fe32bf..a0e5b2df 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
-
<PropertyGroup>
<AssemblyName>SMAPI.Tests</AssemblyName>
<RootNamespace>SMAPI.Tests</RootNamespace>
<TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion>
- <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
@@ -31,7 +31,4 @@
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
-
- <Import Project="..\..\build\common.targets" />
-
</Project>
diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj
index 2bddc46a..d36a1882 100644
--- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj
+++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj
@@ -4,7 +4,6 @@
<Description>Provides toolkit interfaces which are available to SMAPI mods.</Description>
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
</PropertyGroup>
<Import Project="..\..\build\common.targets" />
diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
index b01d8b21..e635725c 100644
--- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
@@ -89,13 +89,6 @@ namespace StardewModdingAPI.Toolkit.Framework
: "StardewValley.exe";
}
- /// <summary>Get whether the platform uses Mono.</summary>
- /// <param name="platform">The current platform.</param>
- public static bool IsMono(string platform)
- {
- return platform == nameof(Platform.Linux) || platform == nameof(Platform.Mac);
- }
-
/*********
** Private methods
diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
index 3fc9de58..d8e32acf 100644
--- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -4,9 +4,10 @@
<Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description>
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.28" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
index 4ef578f7..62bd13cd 100644
--- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -46,12 +46,5 @@ namespace StardewModdingAPI.Toolkit.Utilities
{
return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString());
}
-
- /// <summary>Get whether the platform uses Mono.</summary>
- /// <param name="platform">The current platform.</param>
- public static bool IsMono(this Platform platform)
- {
- return LowLevelEnvironmentUtility.IsMono(platform.ToString());
- }
}
}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index 6f9f50f0..ce5ffdbd 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
-
<PropertyGroup>
<AssemblyName>SMAPI.Web</AssemblyName>
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
@@ -7,6 +6,8 @@
<LangVersion>latest</LangVersion>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<Content Remove="aws-beanstalk-tools-defaults.json" />
</ItemGroup>
@@ -45,7 +46,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
-
- <Import Project="..\..\build\common.targets" />
-
</Project>
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 0de2b12f..8b0c952d 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -37,6 +38,14 @@ namespace StardewModdingAPI
/// <summary>The target game platform.</summary>
internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
+ /// <summary>The game framework running the game.</summary>
+ internal static GameFramework GameFramework { get; } =
+#if SMAPI_FOR_XNA
+ GameFramework.Xna;
+#else
+ GameFramework.MonoGame;
+#endif
+
/// <summary>The game's assembly name.</summary>
internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley";
@@ -54,7 +63,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.4");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.5");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4");
@@ -65,6 +74,9 @@ namespace StardewModdingAPI
/// <summary>The target game platform.</summary>
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
+ /// <summary>The game framework running the game.</summary>
+ public static GameFramework GameFramework { get; } = EarlyConstants.GameFramework;
+
/// <summary>The path to the game folder.</summary>
public static string ExecutionPath { get; } = EarlyConstants.ExecutionPath;
@@ -208,56 +220,79 @@ namespace StardewModdingAPI
/// <summary>Get metadata for mapping assemblies to the current platform.</summary>
/// <param name="targetPlatform">The target game platform.</param>
- internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform)
+ /// <param name="framework">The game framework running the game.</param>
+ internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform, GameFramework framework)
{
- // get assembly changes needed for platform
- string[] removeAssemblyReferences;
- Assembly[] targetAssemblies;
+ var removeAssemblyReferences = new List<string>();
+ var targetAssemblies = new List<Assembly>();
+
+ // get assembly renamed in SMAPI 3.0
+ removeAssemblyReferences.Add("StardewModdingAPI.Toolkit.CoreInterfaces");
+ targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly);
+
+ // get changes for platform
switch (targetPlatform)
{
case Platform.Linux:
case Platform.Mac:
- removeAssemblyReferences = new[]
+ removeAssemblyReferences.AddRange(new[]
{
"Netcode",
- "Stardew Valley",
+ "Stardew Valley"
+ });
+ targetAssemblies.Add(
+ typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/Mac
+ );
+ break;
+
+ case Platform.Windows:
+ removeAssemblyReferences.Add(
+ "StardewValley"
+ );
+ targetAssemblies.AddRange(new[]
+ {
+ typeof(Netcode.NetBool).Assembly,
+ typeof(StardewValley.Game1).Assembly
+ });
+ break;
+
+ default:
+ throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'.");
+ }
+
+ // get changes for game framework
+ switch (framework)
+ {
+ case GameFramework.MonoGame:
+ removeAssemblyReferences.AddRange(new[]
+ {
"Microsoft.Xna.Framework",
"Microsoft.Xna.Framework.Game",
"Microsoft.Xna.Framework.Graphics",
- "Microsoft.Xna.Framework.Xact",
- "StardewModdingAPI.Toolkit.CoreInterfaces" // renamed in SMAPI 3.0
- };
- targetAssemblies = new[]
- {
- typeof(StardewValley.Game1).Assembly, // note: includes Netcode types on Linux/Mac
- typeof(Microsoft.Xna.Framework.Vector2).Assembly,
- typeof(StardewModdingAPI.IManifest).Assembly
- };
+ "Microsoft.Xna.Framework.Xact"
+ });
+ targetAssemblies.Add(
+ typeof(Microsoft.Xna.Framework.Vector2).Assembly
+ );
break;
- case Platform.Windows:
- removeAssemblyReferences = new[]
+ case GameFramework.Xna:
+ removeAssemblyReferences.Add(
+ "MonoGame.Framework"
+ );
+ targetAssemblies.AddRange(new[]
{
- "StardewValley",
- "MonoGame.Framework",
- "StardewModdingAPI.Toolkit.CoreInterfaces" // renamed in SMAPI 3.0
- };
- targetAssemblies = new[]
- {
- typeof(Netcode.NetBool).Assembly,
- typeof(StardewValley.Game1).Assembly,
typeof(Microsoft.Xna.Framework.Vector2).Assembly,
typeof(Microsoft.Xna.Framework.Game).Assembly,
- typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly,
- typeof(StardewModdingAPI.IManifest).Assembly
- };
+ typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly
+ });
break;
default:
- throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'.");
+ throw new InvalidOperationException($"Unknown game framework '{framework}'.");
}
- return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
+ return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray());
}
diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs
index b1b33cd6..5f70d0f7 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -38,6 +38,9 @@ namespace StardewModdingAPI
set => Context.LoadStageForScreen.Value = value;
}
+ /// <summary>Whether the in-game world is completely unloaded and not in the process of being loaded. The world may still exist in memory at this point, but should be ignored.</summary>
+ internal static bool IsWorldFullyUnloaded => Context.LoadStage == LoadStage.ReturningToTitle || Context.LoadStage == LoadStage.None;
+
/*********
** Accessors
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index af65e07e..7edc9ab9 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework.Content
this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
// get key normalization logic
- if (Constants.Platform == Platform.Windows)
+ if (Constants.GameFramework == GameFramework.Xna)
{
IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath");
this.NormalizeAssetNameForPlatform = path => method.Invoke<string>(path);
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 32195fff..2920e670 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content;
@@ -207,11 +208,30 @@ namespace StardewModdingAPI.Framework
/// <remarks>This is called after the player returns to the title screen, but before <see cref="Game1.CleanupReturningToTitle"/> runs.</remarks>
public void OnReturningToTitleScreen()
{
- this.ContentManagerLock.InReadLock(() =>
- {
- foreach (IContentManager contentManager in this.ContentManagers)
- contentManager.OnReturningToTitleScreen();
- });
+ // The game clears LocalizedContentManager.localizedAssetNames after returning to the title screen. That
+ // causes an inconsistency in the SMAPI asset cache, which leads to an edge case where assets already
+ // provided by mods via IAssetLoader when playing in non-English are ignored.
+ //
+ // For example, let's say a mod provides the 'Data\mail' asset through IAssetLoader when playing in
+ // Portuguese. Here's the normal load process after it's loaded:
+ // 1. The game requests Data\mail.
+ // 2. SMAPI sees that it's already cached, and calls LoadRaw to bypass asset interception.
+ // 3. LoadRaw sees that there's a localized key mapping, and gets the mapped key.
+ // 4. In this case "Data\mail" is mapped to "Data\mail" since it was loaded by a mod, so it loads that
+ // asset.
+ //
+ // When the game clears localizedAssetNames, that process goes wrong in step 4:
+ // 3. LoadRaw sees that there's no localized key mapping *and* the locale is non-English, so it attempts
+ // to load from the localized key format.
+ // 4. In this case that's 'Data\mail.pt-BR', so it successfully loads that asset.
+ // 5. Since we've bypassed asset interception at this point, it's loaded directly from the base content
+ // manager without mod changes.
+ //
+ // To avoid issues, we just remove affected assets from the cache here so they'll be reloaded normally.
+ // Note that we *must* propagate changes here, otherwise when mods invalidate the cache later to reapply
+ // their changes, the assets won't be found in the cache so no changes will be propagated.
+ if (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.en)
+ this.InvalidateCache((contentManager, key, type) => contentManager is GameContentManager);
}
/// <summary>Get whether this asset is mapped to a mod folder.</summary>
@@ -275,7 +295,7 @@ namespace StardewModdingAPI.Framework
public IEnumerable<string> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
{
string locale = this.GetLocale();
- return this.InvalidateCache((assetName, type) =>
+ return this.InvalidateCache((contentManager, assetName, type) =>
{
IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormalizeAssetName);
return predicate(info);
@@ -286,7 +306,7 @@ namespace StardewModdingAPI.Framework
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the invalidated asset names.</returns>
- public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
+ public IEnumerable<string> InvalidateCache(Func<IContentManager, string, Type, bool> predicate, bool dispose = false)
{
// invalidate cache & track removed assets
IDictionary<string, Type> removedAssets = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
@@ -295,7 +315,7 @@ namespace StardewModdingAPI.Framework
// cached assets
foreach (IContentManager contentManager in this.ContentManagers)
{
- foreach (var entry in contentManager.InvalidateCache(predicate, dispose))
+ foreach (var entry in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose))
{
if (!removedAssets.ContainsKey(entry.Key))
removedAssets[entry.Key] = entry.Value.GetType();
@@ -313,7 +333,7 @@ namespace StardewModdingAPI.Framework
// get map path
string mapPath = this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value);
- if (!removedAssets.ContainsKey(mapPath) && predicate(mapPath, typeof(Map)))
+ if (!removedAssets.ContainsKey(mapPath) && predicate(this.MainContentManager, mapPath, typeof(Map)))
removedAssets[mapPath] = typeof(Map);
}
}
@@ -322,11 +342,34 @@ namespace StardewModdingAPI.Framework
// reload core game assets
if (removedAssets.Any())
{
- IDictionary<string, bool> propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager
- this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace);
+ // propagate changes to the game
+ this.CoreAssets.Propagate(
+ assets: removedAssets.ToDictionary(p => p.Key, p => p.Value),
+ ignoreWorld: Context.IsWorldFullyUnloaded,
+ out IDictionary<string, bool> propagated,
+ out bool updatedNpcWarps
+ );
+
+ // log summary
+ StringBuilder report = new StringBuilder();
+ {
+ string[] invalidatedKeys = removedAssets.Keys.ToArray();
+ string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray();
+
+ string FormatKeyList(IEnumerable<string> keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase));
+
+ report.AppendLine($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)}).");
+ report.AppendLine(propagated.Count > 0
+ ? $"Propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."
+ : "Propagated 0 core assets."
+ );
+ if (updatedNpcWarps)
+ report.AppendLine("Updated NPC pathfinding cache.");
+ }
+ this.Monitor.Log(report.ToString().TrimEnd());
}
else
- this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
+ this.Monitor.Log("Invalidated 0 cache entries.");
return removedAssets.Keys;
}
@@ -372,7 +415,7 @@ namespace StardewModdingAPI.Framework
return;
this.IsDisposed = true;
- this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace);
+ this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.");
foreach (IContentManager contentManager in this.ContentManagers)
contentManager.Dispose();
this.ContentManagers.Clear();
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 1a64dab8..7244a534 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -122,9 +122,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
public virtual void OnLocaleChanged() { }
/// <inheritdoc />
- public virtual void OnReturningToTitleScreen() { }
-
- /// <inheritdoc />
[Pure]
public string NormalizePathSeparators(string path)
{
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 8e78faba..80a9937a 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -137,31 +137,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
/// <inheritdoc />
- public override void OnReturningToTitleScreen()
- {
- // The game clears LocalizedContentManager.localizedAssetNames after returning to the title screen. That
- // causes an inconsistency in the SMAPI asset cache, which leads to an edge case where assets already
- // provided by mods via IAssetLoader when playing in non-English are ignored.
- //
- // For example, let's say a mod provides the 'Data\mail' asset through IAssetLoader when playing in
- // Portuguese. Here's the normal load process after it's loaded:
- // 1. The game requests Data\mail.
- // 2. SMAPI sees that it's already cached, and calls LoadRaw to bypass asset interception.
- // 3. LoadRaw sees that there's a localized key mapping, and gets the mapped key.
- // 4. In this case "Data\mail" is mapped to "Data\mail" since it was loaded by a mod, so it loads that
- // asset.
- //
- // When the game clears localizedAssetNames, that process goes wrong in step 4:
- // 3. LoadRaw sees that there's no localized key mapping *and* the locale is non-English, so it attempts
- // to load from the localized key format.
- // 4. In this case that's 'Data\mail.pt-BR', so it successfully loads that asset.
- // 5. Since we've bypassed asset interception at this point, it's loaded directly from the base content
- // manager without mod changes.
- if (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.en)
- this.InvalidateCache((_, _) => true);
- }
-
- /// <inheritdoc />
public override LocalizedContentManager CreateTemporary()
{
return this.Coordinator.CreateGameContentManager("(temporary)");
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index 1e222472..d7963305 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -69,9 +69,5 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Perform any cleanup needed when the locale changes.</summary>
void OnLocaleChanged();
-
- /// <summary>Clean up when the player is returning to the title screen.</summary>
- /// <remarks>This is called after the player returns to the title screen, but before <see cref="Game1.CleanupReturningToTitle"/> runs.</remarks>
- void OnReturningToTitleScreen();
}
}
diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs
index ba1879da..ab7f1e6c 100644
--- a/src/SMAPI/Framework/InternalExtensions.cs
+++ b/src/SMAPI/Framework/InternalExtensions.cs
@@ -179,15 +179,10 @@ namespace StardewModdingAPI.Framework
/// <param name="reflection">The reflection helper with which to access private fields.</param>
public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection)
{
- // get field name
- const string fieldName =
-#if SMAPI_FOR_WINDOWS
- "inBeginEndPair";
-#else
- "_beginCalled";
-#endif
+ string fieldName = Constants.GameFramework == GameFramework.Xna
+ ? "inBeginEndPair"
+ : "_beginCalled";
- // get result
return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue();
}
}
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 0dd45355..243ca3ae 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -283,8 +283,13 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="customSettings">The custom SMAPI settings.</param>
public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
{
+ // get platform label
+ string platformLabel = EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform);
+ if ((Constants.GameFramework == GameFramework.Xna) != (Constants.Platform == Platform.Windows))
+ platformLabel += $" with {Constants.GameFramework}";
+
// init logging
- 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} on {platformLabel}", LogLevel.Info);
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
if (modsPath != Constants.DefaultModsPath)
this.Monitor.Log("(Using custom --mods-path argument.)");
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 5fd8f5e9..bfca2264 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -136,7 +136,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public bool InvalidateCache<T>()
{
this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace);
- return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)).Any();
+ return this.ContentCore.InvalidateCache((contentManager, key, type) => typeof(T).IsAssignableFrom(type)).Any();
}
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index 69535aa5..3606eb66 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -46,15 +46,16 @@ namespace StardewModdingAPI.Framework.ModLoading
*********/
/// <summary>Construct an instance.</summary>
/// <param name="targetPlatform">The current game platform.</param>
+ /// <param name="framework">The game framework running the game.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
/// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param>
- public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods)
+ public AssemblyLoader(Platform targetPlatform, GameFramework framework, IMonitor monitor, bool paranoidMode, bool rewriteMods)
{
this.Monitor = monitor;
this.ParanoidMode = paranoidMode;
this.RewriteMods = rewriteMods;
- this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform));
+ this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform, framework));
// init resolver
this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver());
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 5df4b61b..ebb21555 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -12,7 +12,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
-#if SMAPI_FOR_WINDOWS
+#if SMAPI_FOR_XNA
using System.Windows.Forms;
#endif
using Newtonsoft.Json;
@@ -217,7 +217,7 @@ namespace StardewModdingAPI.Framework
this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter);
// add error handlers
-#if SMAPI_FOR_WINDOWS
+#if SMAPI_FOR_XNA
Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
#endif
@@ -482,6 +482,7 @@ namespace StardewModdingAPI.Framework
+ ")"
)
)
+ + "."
);
// reload affected assets
@@ -1409,7 +1410,7 @@ namespace StardewModdingAPI.Framework
// load mods
IList<IModMetadata> skippedMods = new List<IModMetadata>();
- using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
+ using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, Constants.GameFramework, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
{
// init
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
diff --git a/src/SMAPI/GameFramework.cs b/src/SMAPI/GameFramework.cs
new file mode 100644
index 00000000..7670ce8f
--- /dev/null
+++ b/src/SMAPI/GameFramework.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI
+{
+ /// <summary>The game framework running the game.</summary>
+ public enum GameFramework
+ {
+ /// <summary>The XNA Framework on Windows.</summary>
+ Xna,
+
+ /// <summary>The MonoGame framework, usually on non-Windows platforms.</summary>
+ MonoGame
+ }
+}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 8b591bc1..52da3946 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -79,8 +79,10 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="assets">The asset keys and types to reload.</param>
- /// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
- public IDictionary<string, bool> Propagate(IDictionary<string, Type> assets)
+ /// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param>
+ /// <param name="propagatedAssets">A lookup of asset names to whether they've been propagated.</param>
+ /// <param name="updatedNpcWarps">Whether the NPC pathfinding cache was reloaded.</param>
+ public void Propagate(IDictionary<string, Type> assets, bool ignoreWorld, out IDictionary<string, bool> propagatedAssets, out bool updatedNpcWarps)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
@@ -95,26 +97,36 @@ namespace StardewModdingAPI.Metadata
});
// reload assets
- IDictionary<string, bool> propagated = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase);
+ propagatedAssets = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase);
+ updatedNpcWarps = false;
foreach (var bucket in buckets)
{
switch (bucket.Key)
{
case AssetBucket.Sprite:
- this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated);
+ if (!ignoreWorld)
+ this.ReloadNpcSprites(bucket.Select(p => p.Key), propagatedAssets);
break;
case AssetBucket.Portrait:
- this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated);
+ if (!ignoreWorld)
+ this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagatedAssets);
break;
default:
foreach (var entry in bucket)
- propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value);
+ {
+ bool changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out bool curChangedMapWarps);
+ propagatedAssets[entry.Key] = changed;
+ updatedNpcWarps = updatedNpcWarps || curChangedMapWarps;
+ }
break;
}
}
- return propagated;
+
+ // reload NPC pathfinding cache if any map changed
+ if (updatedNpcWarps)
+ NPC.populateRoutesFromLocationToLocationList();
}
@@ -124,19 +136,22 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="key">The asset key to reload.</param>
/// <param name="type">The asset type to reload.</param>
+ /// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param>
+ /// <param name="changedWarps">Whether any map warps were changed as part of this propagation.</param>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")]
- private bool PropagateOther(string key, Type type)
+ private bool PropagateOther(string key, Type type, bool ignoreWorld, out bool changedWarps)
{
var content = this.MainContentManager;
key = this.AssertAndNormalizeAssetName(key);
+ changedWarps = false;
/****
** Special case: current map tilesheet
** We only need to do this for the current location, since tilesheets are reloaded when you enter a location.
** Just in case, we should still propagate by key even if a tilesheet is matched.
****/
- if (Game1.currentLocation?.map?.TileSheets != null)
+ if (!ignoreWorld && Game1.currentLocation?.map?.TileSheets != null)
{
foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{
@@ -151,14 +166,30 @@ namespace StardewModdingAPI.Metadata
if (type == typeof(Map))
{
bool anyChanged = false;
- foreach (GameLocation location in this.GetLocations())
+
+ if (!ignoreWorld)
{
- if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key)
+ foreach (GameLocation location in this.GetLocations())
{
- this.ReloadMap(location);
- anyChanged = true;
+ if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key)
+ {
+ static ISet<string> GetWarpSet(GameLocation location)
+ {
+ return new HashSet<string>(
+ location.warps.Select(p => $"{p.X} {p.Y} {p.TargetName} {p.TargetX} {p.TargetY}")
+ );
+ }
+
+ var oldWarps = GetWarpSet(location);
+ this.ReloadMap(location);
+ var newWarps = GetWarpSet(location);
+
+ changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p));
+ anyChanged = true;
+ }
}
}
+
return anyChanged;
}
@@ -172,7 +203,7 @@ namespace StardewModdingAPI.Metadata
** Animals
****/
case "animals\\horse":
- return this.ReloadPetOrHorseSprites<Horse>(content, key);
+ return !ignoreWorld && this.ReloadPetOrHorseSprites<Horse>(content, key);
/****
** Buildings
@@ -197,7 +228,7 @@ namespace StardewModdingAPI.Metadata
case "characters\\farmer\\farmer_base_bald":
case "characters\\farmer\\farmer_girl_base":
case "characters\\farmer\\farmer_girl_base_bald":
- return this.ReloadPlayerSprites(key);
+ return !ignoreWorld && this.ReloadPlayerSprites(key);
case "characters\\farmer\\hairstyles": // Game1.LoadContent
FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key);
@@ -270,7 +301,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "data\\farmanimals": // FarmAnimal constructor
- return this.ReloadFarmAnimalData();
+ return !ignoreWorld && this.ReloadFarmAnimalData();
case "data\\hairdata": // Farmer.GetHairStyleMetadataFile
return this.ReloadHairData();
@@ -288,7 +319,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "data\\npcdispositions": // NPC constructor
- return this.ReloadNpcDispositions(content, key);
+ return !ignoreWorld && this.ReloadNpcDispositions(content, key);
case "data\\npcgifttastes": // Game1.LoadContent
Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
@@ -366,6 +397,9 @@ namespace StardewModdingAPI.Metadata
foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton })
button.texture = Game1.mouseCursors;
}
+
+ if (!ignoreWorld)
+ this.ReloadDoorSprites(content, key);
return true;
case "loosesprites\\cursors2": // Game1.LoadContent
@@ -393,7 +427,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
- return this.ReloadSuspensionBridges(content, key);
+ return !ignoreWorld && this.ReloadSuspensionBridges(content, key);
/****
** Content\Maps
@@ -452,14 +486,14 @@ namespace StardewModdingAPI.Metadata
return true;
case "tilesheets\\chairtiles": // Game1.LoadContent
- return this.ReloadChairTiles(content, key);
+ return this.ReloadChairTiles(content, key, ignoreWorld);
case "tilesheets\\craftables": // Game1.LoadContent
Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
return true;
case "tilesheets\\critters": // Critter constructor
- return this.ReloadCritterTextures(content, key) > 0;
+ return !ignoreWorld && this.ReloadCritterTextures(content, key) > 0;
case "tilesheets\\crops": // Game1.LoadContent
Game1.cropSpriteSheet = content.Load<Texture2D>(key);
@@ -513,7 +547,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "terrainfeatures\\grass": // from Grass
- return this.ReloadGrassTextures(content, key);
+ return !ignoreWorld && this.ReloadGrassTextures(content, key);
case "terrainfeatures\\hoedirt": // from HoeDirt
HoeDirt.lightTexture = content.Load<Texture2D>(key);
@@ -528,52 +562,55 @@ namespace StardewModdingAPI.Metadata
return true;
case "terrainfeatures\\mushroom_tree": // from Tree
- return this.ReloadTreeTextures(content, key, Tree.mushroomTree);
+ return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.mushroomTree);
case "terrainfeatures\\tree_palm": // from Tree
- return this.ReloadTreeTextures(content, key, Tree.palmTree);
+ return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.palmTree);
case "terrainfeatures\\tree1_fall": // from Tree
case "terrainfeatures\\tree1_spring": // from Tree
case "terrainfeatures\\tree1_summer": // from Tree
case "terrainfeatures\\tree1_winter": // from Tree
- return this.ReloadTreeTextures(content, key, Tree.bushyTree);
+ return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.bushyTree);
case "terrainfeatures\\tree2_fall": // from Tree
case "terrainfeatures\\tree2_spring": // from Tree
case "terrainfeatures\\tree2_summer": // from Tree
case "terrainfeatures\\tree2_winter": // from Tree
- return this.ReloadTreeTextures(content, key, Tree.leafyTree);
+ return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.leafyTree);
case "terrainfeatures\\tree3_fall": // from Tree
case "terrainfeatures\\tree3_spring": // from Tree
case "terrainfeatures\\tree3_winter": // from Tree
- return this.ReloadTreeTextures(content, key, Tree.pineTree);
+ return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.pineTree);
}
/****
** Dynamic assets
****/
- // dynamic textures
- if (this.KeyStartsWith(key, "animals\\cat"))
- return this.ReloadPetOrHorseSprites<Cat>(content, key);
- if (this.KeyStartsWith(key, "animals\\dog"))
- return this.ReloadPetOrHorseSprites<Dog>(content, key);
- if (this.IsInFolder(key, "Animals"))
- return this.ReloadFarmAnimalSprites(content, key);
-
- if (this.IsInFolder(key, "Buildings"))
- return this.ReloadBuildings(content, key);
-
- if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
- return this.ReloadFenceTextures(key);
-
- // dynamic data
- if (this.IsInFolder(key, "Characters\\Dialogue"))
- return this.ReloadNpcDialogue(key);
-
- if (this.IsInFolder(key, "Characters\\schedules"))
- return this.ReloadNpcSchedules(key);
+ if (!ignoreWorld)
+ {
+ // dynamic textures
+ if (this.KeyStartsWith(key, "animals\\cat"))
+ return this.ReloadPetOrHorseSprites<Cat>(content, key);
+ if (this.KeyStartsWith(key, "animals\\dog"))
+ return this.ReloadPetOrHorseSprites<Dog>(content, key);
+ if (this.IsInFolder(key, "Animals"))
+ return this.ReloadFarmAnimalSprites(content, key);
+
+ if (this.IsInFolder(key, "Buildings"))
+ return this.ReloadBuildings(content, key);
+
+ if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
+ return this.ReloadFenceTextures(key);
+
+ // dynamic data
+ if (this.IsInFolder(key, "Characters\\Dialogue"))
+ return this.ReloadNpcDialogue(key);
+
+ if (this.IsInFolder(key, "Characters\\schedules"))
+ return this.ReloadNpcSchedules(key);
+ }
return false;
}
@@ -693,19 +730,23 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload map seat textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
+ /// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param>
/// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadChairTiles(LocalizedContentManager content, string key)
+ private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld)
{
MapSeat.mapChairTexture = content.Load<Texture2D>(key);
- foreach (var location in this.GetLocations())
+ if (!ignoreWorld)
{
- foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
+ foreach (var location in this.GetLocations())
{
- string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile);
+ foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
+ {
+ string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile);
- if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase))
- seat.overlayTexture = MapSeat.mapChairTexture;
+ if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase))
+ seat.overlayTexture = MapSeat.mapChairTexture;
+ }
}
}
@@ -739,6 +780,36 @@ namespace StardewModdingAPI.Metadata
return critters.Length;
}
+ /// <summary>Reload the sprites for interior doors.</summary>
+ /// <param name="content">The content manager through which to reload the asset.</param>
+ /// <param name="key">The asset key to reload.</param>
+ /// <returns>Returns whether any doors were affected.</returns>
+ private bool ReloadDoorSprites(LocalizedContentManager content, string key)
+ {
+ Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
+
+ foreach (GameLocation location in this.GetLocations())
+ {
+ IEnumerable<InteriorDoor> doors = location.interiorDoors?.Doors;
+ if (doors == null)
+ continue;
+
+ foreach (InteriorDoor door in doors)
+ {
+ if (door?.Sprite == null)
+ continue;
+
+ string textureName = this.NormalizeAssetNameIgnoringEmpty(this.Reflection.GetField<string>(door.Sprite, "textureName").GetValue());
+ if (textureName != key)
+ continue;
+
+ door.Sprite.texture = texture.Value;
+ }
+ }
+
+ return texture.IsValueCreated;
+ }
+
/// <summary>Reload the data for matching farm animals.</summary>
/// <returns>Returns whether any farm animals were affected.</returns>
/// <remarks>Derived from the <see cref="FarmAnimal"/> constructor.</remarks>
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index a9e6f389..034eceed 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -41,9 +41,10 @@ copy all the settings, or you may cause bugs due to overridden changes in future
/**
* Whether to enable more aggressive memory optimizations.
- * You can try disabling this if you get ObjectDisposedException errors.
+ * If you get frequent 'OutOfMemoryException' errors, you can try enabling this to reduce their
+ * frequency. This may cause crashes for farmhands in multiplayer.
*/
- "AggressiveMemoryOptimizations": true,
+ "AggressiveMemoryOptimizations": false,
/**
* Whether to add a section to the 'mod issues' list for mods which directly use potentially
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index 6344cb2f..ceef33df 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -12,6 +12,8 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
+ <Import Project="..\..\build\common.targets" />
+
<ItemGroup>
<PackageReference Include="LargeAddressAware" Version="1.0.5" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
@@ -30,20 +32,22 @@
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" />
</ItemGroup>
+ <!-- Windows only -->
+ <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+ <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
+ <Reference Include="System.Windows.Forms" />
+ </ItemGroup>
+
+ <!-- Game framework -->
<Choose>
- <!-- Windows -->
- <When Condition="$(OS) == 'Windows_NT'">
+ <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
<ItemGroup>
- <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
- <Reference Include="System.Windows.Forms" />
</ItemGroup>
</When>
-
- <!-- Linux/Mac -->
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
@@ -67,5 +71,4 @@
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
- <Import Project="..\..\build\common.targets" />
</Project>
diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs
index 1845285a..28cae240 100644
--- a/src/SMAPI/Utilities/KeybindList.cs
+++ b/src/SMAPI/Utilities/KeybindList.cs
@@ -30,6 +30,11 @@ namespace StardewModdingAPI.Utilities
this.IsBound = this.Keybinds.Any();
}
+ /// <summary>Construct an instance.</summary>
+ /// <param name="singleKey">A single-key binding.</param>
+ public KeybindList(SButton singleKey)
+ : this(new Keybind(singleKey)) { }
+
/// <summary>Parse a keybind list from a string, and throw an exception if it's not valid.</summary>
/// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
/// <exception cref="FormatException">The <paramref name="input"/> format is invalid.</exception>