summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-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
10 files changed, 74 insertions, 61 deletions
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);