diff --git a/build/common.targets b/build/common.targets
index d9d214666..d680fa749 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -4,12 +4,12 @@
- 3.9.4
+ 3.9.5
SMAPI
latest
$(AssemblySearchPaths);{GAC}
- $(DefineConstants);SMAPI_FOR_WINDOWS
+ $(DefineConstants);SMAPI_FOR_WINDOWS;SMAPI_FOR_XNA
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 045a5168e..ad6445326 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 e2832710b..e77d9d826 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
`` 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 44ed3bd10..1777be5fc 100644
--- a/src/SMAPI.Installer/SMAPI.Installer.csproj
+++ b/src/SMAPI.Installer/SMAPI.Installer.csproj
@@ -4,7 +4,6 @@
The SMAPI installer for players.
net45
Exe
- x86
false
diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
index 1813f58b1..5992fbbf5 100644
--- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
+++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
@@ -3,7 +3,6 @@
StardewModdingAPI.ModBuildConfig
net45
- x86
latest
true
diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets
index 65544b12e..76a1536cd 100644
--- a/src/SMAPI.ModBuildConfig/build/smapi.targets
+++ b/src/SMAPI.ModBuildConfig/build/smapi.targets
@@ -47,19 +47,27 @@
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
@@ -41,5 +44,4 @@
-
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 2811a11c5..65c66d33c 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 95e4f5ef1..8056fd713 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
@@ -27,11 +27,9 @@ public void Apply(HarmonyInstance harmony)
#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 @@ public void Apply(HarmonyInstance harmony)
/*********
** Private methods
*********/
-#if SMAPI_FOR_WINDOWS
+#if SMAPI_FOR_XNA
/// The method to call instead of .
/// The texture to validate.
#else
diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj
index 5c0cf952a..788f6f161 100644
--- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj
+++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj
@@ -4,9 +4,10 @@
StardewModdingAPI.Mods.ErrorHandler
net45
false
- x86
+
+
@@ -16,19 +17,21 @@
+
+
+
+
+
+
-
-
+
-
-
-
@@ -42,5 +45,4 @@
-
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index 52bf4f6af..1e8101134 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 98a3f0cc6..a6f767813 100644
--- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj
+++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj
@@ -4,9 +4,10 @@
StardewModdingAPI.Mods.SaveBackup
net45
false
- x86
+
+
@@ -20,5 +21,4 @@
-
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index b88582ba4..ced7888ac 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 51fe32bf4..a0e5b2df5 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -1,14 +1,14 @@
-
SMAPI.Tests
SMAPI.Tests
net45
false
latest
- x86
+
+
@@ -31,7 +31,4 @@
-
-
-
diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj
index 2bddc46a0..d36a1882e 100644
--- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj
+++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj
@@ -4,7 +4,6 @@
Provides toolkit interfaces which are available to SMAPI mods.
net4.5;netstandard2.0
true
- x86
diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
index b01d8b21a..e635725c4 100644
--- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
@@ -89,13 +89,6 @@ public static string GetExecutableName(string platform)
: "StardewValley.exe";
}
- /// Get whether the platform uses Mono.
- /// The current platform.
- 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 3fc9de585..d8e32acf1 100644
--- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -4,9 +4,10 @@
A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.
net4.5;netstandard2.0
true
- x86
+
+
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
index 4ef578f7a..62bd13cd9 100644
--- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -46,12 +46,5 @@ public static string GetExecutableName(Platform platform)
{
return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString());
}
-
- /// Get whether the platform uses Mono.
- /// The current platform.
- 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 6f9f50f04..ce5ffdbd3 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -1,5 +1,4 @@
-
SMAPI.Web
StardewModdingAPI.Web
@@ -7,6 +6,8 @@
latest
+
+
@@ -45,7 +46,4 @@
PreserveNewest
-
-
-
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 0de2b12f8..8b0c952dc 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 @@ internal static class EarlyConstants
/// The target game platform.
internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
+ /// The game framework running the game.
+ internal static GameFramework GameFramework { get; } =
+#if SMAPI_FOR_XNA
+ GameFramework.Xna;
+#else
+ GameFramework.MonoGame;
+#endif
+
/// The game's assembly name.
internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley";
@@ -54,7 +63,7 @@ public static class Constants
** Public
****/
/// SMAPI's current semantic version.
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.4");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.5");
/// The minimum supported version of Stardew Valley.
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4");
@@ -65,6 +74,9 @@ public static class Constants
/// The target game platform.
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
+ /// The game framework running the game.
+ public static GameFramework GameFramework { get; } = EarlyConstants.GameFramework;
+
/// The path to the game folder.
public static string ExecutionPath { get; } = EarlyConstants.ExecutionPath;
@@ -208,56 +220,79 @@ internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion versio
/// Get metadata for mapping assemblies to the current platform.
/// The target game platform.
- internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform)
+ /// The game framework running the game.
+ internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform, GameFramework framework)
{
- // get assembly changes needed for platform
- string[] removeAssemblyReferences;
- Assembly[] targetAssemblies;
+ var removeAssemblyReferences = new List();
+ var targetAssemblies = new List();
+
+ // 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 b1b33cd68..5f70d0f7f 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -38,6 +38,9 @@ internal static LoadStage LoadStage
set => Context.LoadStageForScreen.Value = value;
}
+ /// 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.
+ 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 af65e07e0..7edc9ab91 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -52,7 +52,7 @@ public ContentCache(LocalizedContentManager contentManager, Reflector reflection
this.Cache = reflection.GetField>(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(path);
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 32195fff3..2920e6704 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -4,6 +4,7 @@
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 @@ public void OnLocaleChanged()
/// This is called after the player returns to the title screen, but before runs.
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);
}
/// Get whether this asset is mapped to a mod folder.
@@ -275,7 +295,7 @@ public T LoadManagedAsset(string contentManagerID, string relativePath)
public IEnumerable InvalidateCache(Func 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 @@ public IEnumerable InvalidateCache(Func predicate, boo
/// Matches the asset keys to invalidate.
/// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game.
/// Returns the invalidated asset names.
- public IEnumerable InvalidateCache(Func predicate, bool dispose = false)
+ public IEnumerable InvalidateCache(Func predicate, bool dispose = false)
{
// invalidate cache & track removed assets
IDictionary removedAssets = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -295,7 +315,7 @@ public IEnumerable InvalidateCache(Func predicate, b
// 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 @@ public IEnumerable InvalidateCache(Func predicate, b
// 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 @@ public IEnumerable InvalidateCache(Func predicate, b
// reload core game assets
if (removedAssets.Any())
{
- IDictionary 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 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 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 @@ public void Dispose()
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 1a64dab88..7244a534c 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -121,9 +121,6 @@ public override T LoadBase(string assetName)
///
public virtual void OnLocaleChanged() { }
- ///
- public virtual void OnReturningToTitleScreen() { }
-
///
[Pure]
public string NormalizePathSeparators(string path)
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 8e78fabaf..80a9937a5 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -136,31 +136,6 @@ public override void OnLocaleChanged()
this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.");
}
- ///
- 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);
- }
-
///
public override LocalizedContentManager CreateTemporary()
{
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index 1e2224727..d7963305f 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -69,9 +69,5 @@ internal interface IContentManager : IDisposable
/// Perform any cleanup needed when the locale changes.
void OnLocaleChanged();
-
- /// Clean up when the player is returning to the title screen.
- /// This is called after the player returns to the title screen, but before runs.
- void OnReturningToTitleScreen();
}
}
diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs
index ba1879da0..ab7f1e6cd 100644
--- a/src/SMAPI/Framework/InternalExtensions.cs
+++ b/src/SMAPI/Framework/InternalExtensions.cs
@@ -179,15 +179,10 @@ static IEnumerable GetAncestors(IClickableMenu menu)
/// The reflection helper with which to access private fields.
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(Game1.spriteBatch, fieldName).GetValue();
}
}
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 0dd453559..243ca3ae5 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -283,8 +283,13 @@ public void LogFatalLaunchError(Exception exception)
/// The custom SMAPI settings.
public void LogIntro(string modsPath, IDictionary 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 5fd8f5e96..bfca22648 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -136,7 +136,7 @@ public bool InvalidateCache(string key)
public bool InvalidateCache()
{
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();
}
///
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index 69535aa5f..3606eb665 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -46,15 +46,16 @@ internal class AssemblyLoader : IDisposable
*********/
/// Construct an instance.
/// The current game platform.
+ /// The game framework running the game.
/// Encapsulates monitoring and logging.
/// Whether to detect paranoid mode issues.
/// Whether to rewrite mods for compatibility.
- 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 5df4b61b3..ebb21555b 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -12,7 +12,7 @@
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 @@ public void RunInteractively()
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 @@ private void OnGameUpdating(GameTime gameTime, Action runGameUpdate)
+ ")"
)
)
+ + "."
);
// reload affected assets
@@ -1409,7 +1410,7 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin
// load mods
IList skippedMods = new List();
- 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 suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
diff --git a/src/SMAPI/GameFramework.cs b/src/SMAPI/GameFramework.cs
new file mode 100644
index 000000000..7670ce8f5
--- /dev/null
+++ b/src/SMAPI/GameFramework.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI
+{
+ /// The game framework running the game.
+ public enum GameFramework
+ {
+ /// The XNA Framework on Windows.
+ Xna,
+
+ /// The MonoGame framework, usually on non-Windows platforms.
+ MonoGame
+ }
+}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 8b591bc12..52da39462 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -79,8 +79,10 @@ public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManag
/// Reload one of the game's core assets (if applicable).
/// The asset keys and types to reload.
- /// Returns a lookup of asset names to whether they've been propagated.
- public IDictionary Propagate(IDictionary assets)
+ /// 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.
+ /// A lookup of asset names to whether they've been propagated.
+ /// Whether the NPC pathfinding cache was reloaded.
+ public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool updatedNpcWarps)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
@@ -95,26 +97,36 @@ public IDictionary Propagate(IDictionary assets)
});
// reload assets
- IDictionary 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 @@ public IDictionary Propagate(IDictionary assets)
/// Reload one of the game's core assets (if applicable).
/// The asset key to reload.
/// The asset type to reload.
+ /// 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.
+ /// Whether any map warps were changed as part of this propagation.
/// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.
[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 @@ private bool PropagateOther(string key, Type type)
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 GetWarpSet(GameLocation location)
+ {
+ return new HashSet(
+ 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 @@ private bool PropagateOther(string key, Type type)
** Animals
****/
case "animals\\horse":
- return this.ReloadPetOrHorseSprites(content, key);
+ return !ignoreWorld && this.ReloadPetOrHorseSprites(content, key);
/****
** Buildings
@@ -197,7 +228,7 @@ private bool PropagateOther(string key, Type type)
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 @@ private bool PropagateOther(string key, Type type)
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 @@ private bool PropagateOther(string key, Type type)
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>(key);
@@ -366,6 +397,9 @@ private bool PropagateOther(string key, Type type)
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 @@ private bool PropagateOther(string key, Type type)
return true;
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
- return this.ReloadSuspensionBridges(content, key);
+ return !ignoreWorld && this.ReloadSuspensionBridges(content, key);
/****
** Content\Maps
@@ -452,14 +486,14 @@ private bool PropagateOther(string key, Type type)
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(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(key);
@@ -513,7 +547,7 @@ private bool PropagateOther(string key, Type type)
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(key);
@@ -528,52 +562,55 @@ private bool PropagateOther(string key, Type type)
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(content, key);
- if (this.KeyStartsWith(key, "animals\\dog"))
- return this.ReloadPetOrHorseSprites(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(content, key);
+ if (this.KeyStartsWith(key, "animals\\dog"))
+ return this.ReloadPetOrHorseSprites(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 @@ private bool ReloadBuildings(LocalizedContentManager content, string key)
/// Reload map seat textures.
/// The content manager through which to reload the asset.
/// The asset key to reload.
+ /// 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.
/// Returns whether any textures were reloaded.
- private bool ReloadChairTiles(LocalizedContentManager content, string key)
+ private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld)
{
MapSeat.mapChairTexture = content.Load(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 @@ select critter
return critters.Length;
}
+ /// Reload the sprites for interior doors.
+ /// The content manager through which to reload the asset.
+ /// The asset key to reload.
+ /// Returns whether any doors were affected.
+ private bool ReloadDoorSprites(LocalizedContentManager content, string key)
+ {
+ Lazy texture = new Lazy(() => content.Load(key));
+
+ foreach (GameLocation location in this.GetLocations())
+ {
+ IEnumerable 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(door.Sprite, "textureName").GetValue());
+ if (textureName != key)
+ continue;
+
+ door.Sprite.texture = texture.Value;
+ }
+ }
+
+ return texture.IsValueCreated;
+ }
+
/// Reload the data for matching farm animals.
/// Returns whether any farm animals were affected.
/// Derived from the constructor.
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index a9e6f389e..034eceed6 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 6344cb2f6..ceef33df7 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -12,6 +12,8 @@
icon.ico
+
+
@@ -30,20 +32,22 @@
+
+
+
+
+
+
+
-
-
+
-
-
-
-
@@ -67,5 +71,4 @@
-
diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs
index 1845285a0..28cae2402 100644
--- a/src/SMAPI/Utilities/KeybindList.cs
+++ b/src/SMAPI/Utilities/KeybindList.cs
@@ -30,6 +30,11 @@ public KeybindList(params Keybind[] keybinds)
this.IsBound = this.Keybinds.Any();
}
+ /// Construct an instance.
+ /// A single-key binding.
+ public KeybindList(SButton singleKey)
+ : this(new Keybind(singleKey)) { }
+
/// Parse a keybind list from a string, and throw an exception if it's not valid.
/// The keybind string. See remarks on for format details.
/// The format is invalid.