From 467ad2ffd45f7c034b89b668883bb5271524821d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 17:36:31 -0400 Subject: let mods invalidate cached assets by name or type (#335) --- .../Framework/ModHelpers/ContentHelper.cs | 27 +++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework/ModHelpers') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 5f72176e..c052759f 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The friendly mod name for use in errors. private readonly string ModName; + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + /********* ** Accessors @@ -58,13 +61,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The absolute path to the mod folder. /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) + /// Encapsulates monitoring and logging. + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.Monitor = monitor; } /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. @@ -176,6 +181,26 @@ namespace StardewModdingAPI.Framework.ModHelpers } } + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + public bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder) + { + this.Monitor.Log($"Requested cache invalidation for '{key}' in {source}.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, source); + return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + 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.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + } /********* ** Private methods -- cgit From 17acf248b66861217d48826e77f24cc311b4a22e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 31 Jul 2017 21:54:46 -0400 Subject: prevent mods from accessing SMAPI internals using its own reflection helper (#334) --- release-notes.md | 8 ++-- .../Framework/ModHelpers/ReflectionHelper.cs | 46 +++++++++++++++++++++- src/StardewModdingAPI/Program.cs | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework/ModHelpers') diff --git a/release-notes.md b/release-notes.md index 75b9bbd1..bb7b7dfa 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,10 +17,12 @@ For mod developers: * Added support for optional dependencies. * Added support for string versions (like `"1.0-alpha"`) in `manifest.json`. * Added `IEquatable` to `ISemanticVersion`. -* Removed all deprecated code. -* Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. -* Removed support for mods with a non-unique `UniqueID` value in their manifest. * Removed the TrainerMod `save` and `load` commands. +* **Breaking changes:** + * Removed all deprecated code. + * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. + * Removed support for mods with a non-unique `UniqueID` value in their manifest. + * Mods can no longer access SMAPI internals using the reflection helper, to discourage fragile mods. ## 1.15.2 For players: diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs index 9411a97a..14a339da 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -13,16 +13,21 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The underlying reflection helper. private readonly Reflector Reflector; + /// The mod name for error messages. + private readonly string ModName; + /********* ** Public methods *********/ /// Construct an instance. /// The unique ID of the relevant mod. + /// The mod name for error messages. /// The underlying reflection helper. - public ReflectionHelper(string modID, Reflector reflector) + public ReflectionHelper(string modID, string modName, Reflector reflector) : base(modID) { + this.ModName = modName; this.Reflector = reflector; } @@ -37,6 +42,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Returns the field wrapper, or null if the field doesn't exist and is false. public IPrivateField GetPrivateField(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateField(obj, name, required); } @@ -47,6 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateField GetPrivateField(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateField(type, name, required); } @@ -60,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateProperty(obj, name, required); } @@ -70,6 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateProperty(type, name, required); } @@ -89,6 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); IPrivateField field = this.GetPrivateField(obj, name, required); return field != null ? field.GetValue() @@ -107,6 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); IPrivateField field = this.GetPrivateField(type, name, required); return field != null ? field.GetValue() @@ -122,6 +133,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, required); } @@ -131,6 +143,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, required); } @@ -144,6 +157,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); } @@ -154,7 +168,35 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); } + + + /********* + ** Private methods + *********/ + /// Assert that mods can use the reflection helper to access the given type. + /// The type being accessed. + private void AssertAccessAllowed(Type type) + { +#if !SMAPI_1_x + // validate type namespace + if (type.Namespace != null) + { + string rootSmapiNamespace = typeof(Program).Namespace; + if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); + } +#endif + } + + /// Assert that mods can use the reflection helper to access the given type. + /// The object being accessed. + private void AssertAccessAllowed(object obj) + { + if (obj != null) + this.AssertAccessAllowed(obj.GetType()); + } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 969695aa..b51917d9 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -710,7 +710,7 @@ namespace StardewModdingAPI IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); -- cgit From 3599daee459ee27bfe9374c675eb71d086fcfd81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 Aug 2017 00:51:27 -0400 Subject: remove support for invalidating mod assets per discussion (#335) --- src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs | 9 ++++----- src/StardewModdingAPI/IContentHelper.cs | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI/Framework/ModHelpers') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index c052759f..0456ce14 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -182,14 +182,13 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. + /// The asset key to invalidate in the content folder. /// The is empty or contains invalid characters. /// Returns whether the given asset key was cached. - public bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder) + public bool InvalidateCache(string key) { - this.Monitor.Log($"Requested cache invalidation for '{key}' in {source}.", LogLevel.Trace); - string actualKey = this.GetActualAssetKey(key, source); + this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); } diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index 9fe29e4d..beaaf5d4 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -23,11 +23,10 @@ namespace StardewModdingAPI #if !SMAPI_1_x /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. + /// The asset key to invalidate in the content folder. /// The is empty or contains invalid characters. /// Returns whether the given asset key was cached. - bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder); + bool InvalidateCache(string key); /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. /// The asset type to remove from the cache. -- cgit From baeaf826a9bb185c78732b5f2b91c3f499246f1a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 7 Aug 2017 14:12:18 -0400 Subject: add asset editors & loaders to content API in 2.0 (#255) --- .../Framework/ModHelpers/ContentHelper.cs | 4 ++-- src/StardewModdingAPI/IContentHelper.cs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Framework/ModHelpers') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 0456ce14..1e987f00 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -47,10 +47,10 @@ namespace StardewModdingAPI.Framework.ModHelpers internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); /// Interceptors which provide the initial versions of matching content assets. - internal IList AssetLoaders => this.ObservableAssetLoaders; + public IList AssetLoaders => this.ObservableAssetLoaders; /// Interceptors which edit matching content assets after they're loaded. - internal IList AssetEditors => this.ObservableAssetEditors; + public IList AssetEditors => this.ObservableAssetEditors; /********* diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index beaaf5d4..f32c1d18 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; @@ -7,6 +8,21 @@ namespace StardewModdingAPI /// Provides an API for loading content assets. public interface IContentHelper : IModLinked { + /********* + ** Accessors + *********/ +#if !SMAPI_1_x + /// Interceptors which provide the initial versions of matching content assets. + IList AssetLoaders { get; } + + /// Interceptors which edit matching content assets after they're loaded. + IList AssetEditors { get; } +#endif + + + /********* + ** Public methods + *********/ /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. -- cgit From 021672e43db021ae436e23ff45f3d85e2a595cb0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 14 Aug 2017 01:57:11 -0400 Subject: add content helper properties for the current language --- release-notes.md | 1 + src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs | 6 ++++++ src/StardewModdingAPI/IContentHelper.cs | 7 +++++++ 3 files changed, 14 insertions(+) (limited to 'src/StardewModdingAPI/Framework/ModHelpers') diff --git a/release-notes.md b/release-notes.md index 90ca4e2c..9bfd1981 100644 --- a/release-notes.md +++ b/release-notes.md @@ -33,6 +33,7 @@ For players: For mod developers: * Added `Context.CanPlayerMove` value for mod convenience. +* Added `helper.Content` properties for the current language. * Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. * Fixed `Context.IsPlayerFree` being incorrectly false in some cases (e.g. when using a tool). diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 1e987f00..e94d309e 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -40,6 +40,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ + /// The game's current locale code (like pt-BR). + public string CurrentLocale => this.ContentManager.GetLocale(); + + /// The game's current locale as an enum value. + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index f32c1d18..b4557134 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewValley; namespace StardewModdingAPI { @@ -19,6 +20,12 @@ namespace StardewModdingAPI IList AssetEditors { get; } #endif + /// The game's current locale code (like pt-BR). + string CurrentLocale { get; } + + /// The game's current locale as an enum value. + LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } + /********* ** Public methods -- cgit From 2ec0e0e26a16b94ba4a12d3bb4561d64a7411b34 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 Aug 2017 23:27:07 -0400 Subject: only invalidate cache entries matched by new interceptors --- .../Framework/ModHelpers/ContentHelper.cs | 2 +- src/StardewModdingAPI/Framework/SContentManager.cs | 94 ++++++++++++++-------- src/StardewModdingAPI/Program.cs | 26 +++--- 3 files changed, 79 insertions(+), 43 deletions(-) (limited to 'src/StardewModdingAPI/Framework/ModHelpers') 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 } - /// Get the locale codes (like ja-JP) used in asset keys. - /// Simplifies access to private game code. - private IDictionary GetKeyLocales(Reflector reflection) - { - // get the private code field directly to avoid changed-code logic - IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); - - // remember previous settings - LanguageCode previousCode = codeField.GetValue(); - string previousOverride = this.LanguageCodeOverride; - - // create locale => code map - IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.LanguageCodeOverride = null; - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - { - codeField.SetValue(code); - map[this.GetKeyLocale.Invoke()] = code; - } - - // restore previous settings - codeField.SetValue(previousCode); - this.LanguageCodeOverride = previousOverride; - - return map; - } - /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. public string NormalisePathSeparators(string path) @@ -209,10 +183,39 @@ namespace StardewModdingAPI.Framework return GetAllAssetKeys().Distinct(); } - /// Reset the asset cache and reload the game's static assets. + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns whether any cache entries were invalidated. + 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 })); + }); + } + + /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Returns whether any cache entries were invalidated. - /// This implementation is derived from . public bool InvalidateCache(Func predicate) { // find matching asset keys @@ -220,7 +223,7 @@ namespace StardewModdingAPI.Framework HashSet purgeAssetKeys = new HashSet(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()}"); // translated asset } + /// Get the locale codes (like ja-JP) used in asset keys. + /// Simplifies access to private game code. + private IDictionary GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; + } + /// Parse a cache key into its component parts. /// The input cache key. /// The original asset key. 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().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().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); + } } /// Reload translations for all mods. -- cgit