From 04388fe7e3b721358de25d64607d47d5f6113eda Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Mar 2021 04:43:28 -0400 Subject: fix some assets not reapplied correctly when playing in non-English and returning to title --- src/SMAPI/Framework/ContentCoordinator.cs | 37 ++++++++++++++++------ .../ContentManagers/BaseContentManager.cs | 3 -- .../ContentManagers/GameContentManager.cs | 25 --------------- .../Framework/ContentManagers/IContentManager.cs | 4 --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 2 +- 5 files changed, 29 insertions(+), 42 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 32195fff..6d2ff441 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -207,11 +207,30 @@ namespace StardewModdingAPI.Framework /// 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 +294,7 @@ namespace StardewModdingAPI.Framework 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 +305,7 @@ namespace StardewModdingAPI.Framework /// 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 +314,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 +332,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); } } 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 @@ -121,9 +121,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// 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 8e78faba..80a9937a 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -136,31 +136,6 @@ namespace StardewModdingAPI.Framework.ContentManagers 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 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 /// 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/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() { 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(); } /// -- cgit From 749f0321f01b2f2ad865f31ab6f447c5a590fdd0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 Mar 2021 18:56:56 -0400 Subject: avoid asset propagation into the world if it's unloaded Propagating changes into world locations has no effect at this point (since they'll just be recreated when a save is loaded), and can noticeably impact performance. --- src/SMAPI/Framework/ContentCoordinator.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 6d2ff441..5d4855ef 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -341,11 +341,16 @@ namespace StardewModdingAPI.Framework // 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); + IDictionary propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded); + + 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)); + this.Monitor.Log($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)}); propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."); } else - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + this.Monitor.Log("Invalidated 0 cache entries."); return removedAssets.Keys; } @@ -391,7 +396,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(); -- cgit From c39b2b17663f79da92f3d0abe8c01ea73187cbab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Mar 2021 20:16:13 -0400 Subject: update NPC pathfinding cache when map warps change --- src/SMAPI/Framework/ContentCoordinator.cs | 31 +++++++++++++++++++++++++------ src/SMAPI/Framework/SCore.cs | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 5d4855ef..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; @@ -341,13 +342,31 @@ namespace StardewModdingAPI.Framework // reload core game assets if (removedAssets.Any()) { - IDictionary propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded); - - string[] invalidatedKeys = removedAssets.Keys.ToArray(); - string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); + // 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 + ); - string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); - this.Monitor.Log($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)}); propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."); + // 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."); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5df4b61b..e98dc04c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -482,6 +482,7 @@ namespace StardewModdingAPI.Framework + ")" ) ) + + "." ); // reload affected assets -- cgit From 73321eceb96f263f10857667d7b3726a5098e770 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 Mar 2021 20:36:31 -0400 Subject: split compile flag into separate Windows + XNA flags (#767) --- src/SMAPI/Framework/InternalExtensions.cs | 2 +- src/SMAPI/Framework/SCore.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ba1879da..449aa2b7 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -181,7 +181,7 @@ namespace StardewModdingAPI.Framework { // get field name const string fieldName = -#if SMAPI_FOR_WINDOWS +#if SMAPI_FOR_XNA "inBeginEndPair"; #else "_beginCalled"; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index e98dc04c..c758a793 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 -- cgit From ca67dcc920560f3fd19a4531c40fdb03d19e96c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 Mar 2021 20:36:32 -0400 Subject: add Constants.GameFramework field (#767) --- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/InternalExtensions.cs | 11 +++-------- src/SMAPI/Framework/Logging/LogManager.cs | 7 ++++++- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 5 +++-- src/SMAPI/Framework/SCore.cs | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework') 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>(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/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 449aa2b7..ab7f1e6c 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -179,15 +179,10 @@ namespace StardewModdingAPI.Framework /// 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_XNA - "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 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 /// 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/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 *********/ /// 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 c758a793..ebb21555 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1410,7 +1410,7 @@ namespace StardewModdingAPI.Framework // 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); -- cgit