diff options
Diffstat (limited to 'src')
5 files changed, 29 insertions, 42 deletions
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 /// <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 +294,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 +305,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 +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 @@ -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/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 /> |