summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-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
17 files changed, 296 insertions, 153 deletions
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>