diff options
-rw-r--r-- | src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/SContentManager.cs | 94 | ||||
-rw-r--r-- | src/StardewModdingAPI/Program.cs | 26 |
3 files changed, 79 insertions, 43 deletions
diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index e94d309e..ffa78ff6 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0854c379..9e086870 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; @@ -96,33 +97,6 @@ namespace StardewModdingAPI.Framework } - /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> - /// <param name="reflection">Simplifies access to private game code.</param> - private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection) - { - // get the private code field directly to avoid changed-code logic - IPrivateField<LanguageCode> codeField = reflection.GetPrivateField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); - - // remember previous settings - LanguageCode previousCode = codeField.GetValue(); - string previousOverride = this.LanguageCodeOverride; - - // create locale => code map - IDictionary<string, LanguageCode> map = new Dictionary<string, LanguageCode>(StringComparer.InvariantCultureIgnoreCase); - this.LanguageCodeOverride = null; - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - { - codeField.SetValue(code); - map[this.GetKeyLocale.Invoke<string>()] = code; - } - - // restore previous settings - codeField.SetValue(previousCode); - this.LanguageCodeOverride = previousOverride; - - return map; - } - /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="NormaliseAssetName"/> instead.</summary> /// <param name="path">The file path to normalise.</param> public string NormalisePathSeparators(string path) @@ -209,10 +183,39 @@ namespace StardewModdingAPI.Framework return GetAllAssetKeys().Distinct(); } - /// <summary>Reset the asset cache and reload the game's static assets.</summary> + /// <summary>Purge assets from the cache that match one of the interceptors.</summary> + /// <param name="editors">The asset editors for which to purge matching assets.</param> + /// <param name="loaders">The asset loaders for which to purge matching assets.</param> + /// <returns>Returns whether any cache entries were invalidated.</returns> + public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return false; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + + // invalidate matching keys + return this.InvalidateCache((assetName, assetType) => + { + // get asset metadata + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); + + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(assetType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { info }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(assetType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { info })); + }); + } + + /// <summary>Purge matched assets from the cache.</summary> /// <param name="predicate">Matches the asset keys to invalidate.</param> /// <returns>Returns whether any cache entries were invalidated.</returns> - /// <remarks>This implementation is derived from <see cref="Game1.LoadContent"/>.</remarks> public bool InvalidateCache(Func<string, Type, bool> predicate) { // find matching asset keys @@ -220,7 +223,7 @@ namespace StardewModdingAPI.Framework HashSet<string> purgeAssetKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); foreach (string cacheKey in this.Cache.Keys) { - this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); + this.ParseCacheKey(cacheKey, out string assetKey, out _); Type type = this.Cache[cacheKey].GetType(); if (predicate(assetKey, type)) { @@ -237,7 +240,7 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in purgeAssetKeys) { - if(this.CoreAssets.ReloadForKey(this, key)) + if (this.CoreAssets.ReloadForKey(this, key)) reloaded++; } @@ -263,6 +266,33 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset } + /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> + /// <param name="reflection">Simplifies access to private game code.</param> + private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField<LanguageCode> codeField = reflection.GetPrivateField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary<string, LanguageCode> map = new Dictionary<string, LanguageCode>(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke<string>()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; + } + /// <summary>Parse a cache key into its component parts.</summary> /// <param name="cacheKey">The input cache key.</param> /// <param name="assetKey">The original asset key.</param> diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0e1930ac..79f8e801 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -806,33 +806,39 @@ namespace StardewModdingAPI } } - // reset cache when needed - // only register listeners after Entry to avoid repeatedly reloading assets during load + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) { - // TODO: optimise by only reloading assets the new editors/loaders can intercept helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { - this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { - this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray()); } }; } } - this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(editors, loaders); + } } /// <summary>Reload translations for all mods.</summary> |