From 4689eeb6a3af1aead00347fc2575bfebdee50113 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Mar 2019 01:25:12 -0400 Subject: load mods much earlier so they can intercept all content assets --- .../Framework/ContentManagers/GameContentManager.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index ee940cc7..f159f035 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -28,6 +28,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. private readonly IDictionary IsLocalisableLookup; + /// Whether the next load is the first for any game content manager. + private static bool IsFirstLoad = true; + + /// A callback to invoke the first time *any* game content manager loads an asset. + private readonly Action OnLoadingFirstAsset; + /********* ** Public methods @@ -41,10 +47,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates monitoring and logging. /// Simplifies access to private code. /// A callback to invoke when the content manager is being disposed. - public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + /// A callback to invoke the first time *any* game content manager loads an asset. + public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, Action onLoadingFirstAsset) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false) { this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + this.OnLoadingFirstAsset = onLoadingFirstAsset; } /// Load an asset that has been processed by the content pipeline. @@ -53,6 +61,13 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The language code for which to load content. public override T Load(string assetName, LanguageCode language) { + // raise first-load callback + if (GameContentManager.IsFirstLoad) + { + GameContentManager.IsFirstLoad = false; + this.OnLoadingFirstAsset(); + } + // normalise asset name assetName = this.AssertAndNormaliseAssetName(assetName); if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) -- cgit From 6c220453e16c6cb5ad150b61cf02685a97557b3c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Apr 2019 00:40:45 -0400 Subject: fix translatable assets not updated when switching language (#586) --- .../ContentManagers/BaseContentManager.cs | 3 +++ .../ContentManagers/GameContentManager.cs | 23 ++++++++++++++++++++++ .../Framework/ContentManagers/IContentManager.cs | 3 +++ 3 files changed, 29 insertions(+) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 7821e454..b2b3769b 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -138,6 +138,9 @@ namespace StardewModdingAPI.Framework.ContentManagers } } + /// Perform any cleanup needed when the locale changes. + public virtual void OnLocaleChanged() { } + /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index f159f035..55cf15ec 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -112,6 +112,29 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } + /// Perform any cleanup needed when the locale changes. + public override void OnLocaleChanged() + { + base.OnLocaleChanged(); + + // find assets for which a translatable version was loaded + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in this.IsLocalisableLookup.Where(p => p.Value).Select(p => p.Key)) + removeAssetNames.Add(this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) ? assetName : key); + + // invalidate translatable assets + string[] invalidated = this + .InvalidateCache((key, type) => + removeAssetNames.Contains(key) + || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) + ) + .Select(p => p.Item1) + .OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase) + .ToArray(); + if (invalidated.Any()) + this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.", LogLevel.Trace); + } + /// Create a new content manager for temporary use. public override LocalizedContentManager CreateTemporary() { diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 17618edd..66ef9181 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -52,6 +52,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset to clone. T CloneIfPossible(T asset); + /// Perform any cleanup needed when the locale changes. + void OnLocaleChanged(); + /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] -- cgit From 161618d0d92bad5b31e2780c8899c73339d38b62 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Apr 2019 21:45:27 -0400 Subject: fix cache miss when not playing in English (#634) --- .../ContentManagers/BaseContentManager.cs | 4 +-- .../ContentManagers/GameContentManager.cs | 30 ++++++++++++++++++---- .../Framework/ContentManagers/IContentManager.cs | 5 ++-- .../Framework/ContentManagers/ModContentManager.cs | 4 +-- 4 files changed, 31 insertions(+), 12 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index b2b3769b..f54262b8 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -103,11 +103,11 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The type of asset to inject. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The asset value. - public void Inject(string assetName, T value) + /// The language code for which to inject the asset. + public virtual void Inject(string assetName, T value, LanguageCode language) { assetName = this.AssertAndNormaliseAssetName(assetName); this.Cache[assetName] = value; - } /// Get a copy of the given asset if supported. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 55cf15ec..6ce50f00 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, managedAsset); + this.Inject(assetName, managedAsset, language); return managedAsset; } @@ -108,10 +108,30 @@ namespace StardewModdingAPI.Framework.ContentManagers } // update cache & return data - this.Inject(assetName, data); + this.Inject(assetName, data, language); return data; } + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + /// The language code for which to inject the asset. + public override void Inject(string assetName, T value, LanguageCode language) + { + base.Inject(assetName, value, language); + + // track whether the injected asset is translatable for is-loaded lookups + bool isTranslated = this.TryParseExplicitLanguageAssetKey(assetName, out _, out _); + string localisedKey = isTranslated ? assetName : $"{assetName}.{language}"; + if (this.Cache.ContainsKey(localisedKey)) + this.IsLocalisableLookup[localisedKey] = true; + else if (!isTranslated && this.Cache.ContainsKey(assetName)) + this.IsLocalisableLookup[localisedKey] = false; + else + this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); + } + /// Perform any cleanup needed when the locale changes. public override void OnLocaleChanged() { @@ -154,11 +174,11 @@ namespace StardewModdingAPI.Framework.ContentManagers return this.Cache.ContainsKey(normalisedAssetName); // translated - string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) + string localisedKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(localisedKey, out bool localisable)) { return localisable - ? this.Cache.ContainsKey(localeKey) + ? this.Cache.ContainsKey(localisedKey) : this.Cache.ContainsKey(normalisedAssetName); } diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 66ef9181..4c362b0a 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Exceptions; @@ -45,7 +44,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The type of asset to inject. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The asset value. - void Inject(string assetName, T value); + /// The language code for which to inject the asset. + void Inject(string assetName, T value, LocalizedContentManager.LanguageCode language); /// Get a copy of the given asset if supported. /// The asset type. @@ -63,7 +63,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. /// The asset key to check. /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] string AssertAndNormaliseAssetName(string assetName); /// Get the current content locale. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 2c50ec04..1df7d107 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (contentManagerID != this.Name) { T data = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, data); + this.Inject(assetName, data, language); return data; } @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); texture = this.PremultiplyTransparency(texture); - this.Inject(internalKey, texture); + this.Inject(internalKey, texture, language); return (T)(object)texture; } -- cgit From 5cd5e2416dfe6eab1a36be1646890d57f3f4a191 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 May 2019 19:16:08 -0400 Subject: fix cache misses for non-English players --- .../ContentManagers/GameContentManager.cs | 32 ++++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 6ce50f00..085982b6 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -119,15 +119,28 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The language code for which to inject the asset. public override void Inject(string assetName, T value, LanguageCode language) { + // handle explicit language in asset name + { + if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) + { + this.Inject(newAssetName, value, newLanguage); + return; + } + } base.Inject(assetName, value, language); // track whether the injected asset is translatable for is-loaded lookups - bool isTranslated = this.TryParseExplicitLanguageAssetKey(assetName, out _, out _); - string localisedKey = isTranslated ? assetName : $"{assetName}.{language}"; - if (this.Cache.ContainsKey(localisedKey)) - this.IsLocalisableLookup[localisedKey] = true; - else if (!isTranslated && this.Cache.ContainsKey(assetName)) - this.IsLocalisableLookup[localisedKey] = false; + string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; + if (this.Cache.ContainsKey(keyWithLocale)) + { + this.IsLocalisableLookup[assetName] = true; + this.IsLocalisableLookup[keyWithLocale] = true; + } + else if (this.Cache.ContainsKey(assetName)) + { + this.IsLocalisableLookup[assetName] = false; + this.IsLocalisableLookup[keyWithLocale] = false; + } else this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); } @@ -174,11 +187,11 @@ namespace StardewModdingAPI.Framework.ContentManagers return this.Cache.ContainsKey(normalisedAssetName); // translated - string localisedKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalisableLookup.TryGetValue(localisedKey, out bool localisable)) + string keyWithLocale = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(keyWithLocale, out bool localisable)) { return localisable - ? this.Cache.ContainsKey(localisedKey) + ? this.Cache.ContainsKey(keyWithLocale) : this.Cache.ContainsKey(normalisedAssetName); } @@ -190,6 +203,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset key to parse. /// The asset name without the language code. /// The language code removed from the asset name. + /// Returns whether the asset key contains an explicit language and was successfully parsed. private bool TryParseExplicitLanguageAssetKey(string rawAsset, out string assetName, out LanguageCode language) { if (string.IsNullOrWhiteSpace(rawAsset)) -- cgit From fff5e8c93921449afcfd1cebf7fd5616abc15d45 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 29 May 2019 22:32:52 -0400 Subject: move most mod asset loading logic into content managers (#644) This fixes mods needing to load Map assets manually before the game could load them via internal key. --- .../Framework/ContentManagers/ModContentManager.cs | 185 ++++++++++++++++++++- 1 file changed, 179 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1df7d107..45303f6b 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,12 +1,19 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; +using xTile; +using xTile.Format; +using xTile.ObjectModel; +using xTile.Tiles; namespace StardewModdingAPI.Framework.ContentManagers { @@ -19,12 +26,16 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// The game content manager used for map tilesheets not provided by the mod. + private readonly IContentManager GameContentManager; + /********* ** Public methods *********/ /// Construct an instance. /// A name for the mod manager. Not guaranteed to be unique. + /// The game content manager used for map tilesheets not provided by the mod. /// The service provider to use to locate services. /// The root directory to search for content. /// The current culture for which to localise content. @@ -33,9 +44,10 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Simplifies access to private code. /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke when the content manager is being disposed. - public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing) + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) { + this.GameContentManager = gameContentManager; this.JsonHelper = jsonHelper; } @@ -47,11 +59,9 @@ namespace StardewModdingAPI.Framework.ContentManagers { assetName = this.AssertAndNormaliseAssetName(assetName); - // get from cache + // get managed asset if (this.IsLoaded(assetName)) return base.Load(assetName, language); - - // get managed asset if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { if (contentManagerID != this.Name) @@ -64,7 +74,11 @@ namespace StardewModdingAPI.Framework.ContentManagers return this.LoadManagedAsset(assetName, contentManagerID, relativePath, language); } - throw new NotSupportedException("Can't load content folder asset from a mod content manager."); + // get local asset + string internalKey = this.GetInternalAssetKey(assetName); + if (this.IsLoaded(internalKey)) + return base.Load(internalKey, language); + return this.LoadManagedAsset(internalKey, this.Name, assetName, this.Language); } /// Create a new content manager for temporary use. @@ -73,6 +87,16 @@ namespace StardewModdingAPI.Framework.ContentManagers throw new NotSupportedException("Can't create a temporary mod content manager."); } + /// Get the underlying key in the game's content cache for an asset. This does not validate whether the asset exists. + /// The local path to a content file relative to the mod folder. + /// The is empty or contains invalid characters. + public string GetInternalAssetKey(string key) + { + FileInfo file = this.GetModFile(key); + string relativePath = PathUtilities.GetRelativePath(this.RootDirectory, file.FullName); + return Path.Combine(this.Name, relativePath); + } + /********* ** Private methods @@ -133,7 +157,18 @@ namespace StardewModdingAPI.Framework.ContentManagers // unpacked map case ".tbin": - throw GetContentError($"can't read unpacked map file directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); + // validate + if (typeof(T) != typeof(Map)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + this.FixCustomTilesheetPaths(map, relativeMapPath: relativePath); + + // inject map + this.Inject(internalKey, map, this.Language); + return (T)(object)map; default: throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'."); @@ -186,5 +221,143 @@ namespace StardewModdingAPI.Framework.ContentManagers texture.SetData(data); return texture; } + + /// Fix custom map tilesheet paths so they can be found by the content manager. + /// The map whose tilesheets to fix. + /// The relative map path within the mod folder. + /// A map tilesheet couldn't be resolved. + /// + /// The game's logic for tilesheets in is a bit specialised. It boils + /// down to this: + /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded + /// as-is relative to the Content folder. + /// * Else it's loaded from Content\Maps with a seasonal prefix. + /// + /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. + /// Instead we use a more heuristic approach: check relative to the map file first, then relative to + /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, try for a + /// seasonal variation and then an exact match. + /// + /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. + /// + private void FixCustomTilesheetPaths(Map map, string relativeMapPath) + { + // get map info + if (!map.TileSheets.Any()) + return; + relativeMapPath = this.AssertAndNormaliseAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder + bool isOutdoors = map.Properties.TryGetValue("Outdoors", out PropertyValue outdoorsProperty) && outdoorsProperty != null; + + // fix tilesheets + foreach (TileSheet tilesheet in map.TileSheets) + { + string imageSource = tilesheet.ImageSource; + + // validate tilesheet path + if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); + + // get seasonal name (if applicable) + string seasonalImageSource = null; + if (isOutdoors && Context.IsSaveLoaded && Game1.currentSeason != null) + { + string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null."); + bool hasSeasonalPrefix = + filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase); + if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_")) + { + string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase)); + seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}"; + } + } + + // load best match + try + { + string key = + this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource) + ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource); + if (key != null) + { + tilesheet.ImageSource = key; + continue; + } + } + catch (Exception ex) + { + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); + } + + // none found + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder."); + } + } + + /// Get the actual asset name for a tilesheet. + /// The folder path containing the map, relative to the mod folder. + /// The tilesheet image source to load. + /// Returns the asset name. + /// See remarks on . + private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource) + { + if (imageSource == null) + return null; + + // check relative to map file + { + string localKey = Path.Combine(modRelativeMapFolder, imageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + return this.GetInternalAssetKey(localKey); + } + + // check relative to content folder + { + foreach (string candidateKey in new[] { imageSource, Path.Combine("Maps", imageSource) }) + { + string contentKey = candidateKey.EndsWith(".png") + ? candidateKey.Substring(0, candidateKey.Length - 4) + : candidateKey; + + try + { + this.GameContentManager.Load(contentKey); + return contentKey; + } + catch + { + // ignore file-not-found errors + // TODO: while it's useful to suppress an asset-not-found error here to avoid + // confusion, this is a pretty naive approach. Even if the file doesn't exist, + // the file may have been loaded through an IAssetLoader which failed. So even + // if the content file doesn't exist, that doesn't mean the error here is a + // content-not-found error. Unfortunately XNA doesn't provide a good way to + // detect the error type. + if (this.GetContentFolderFileExists(contentKey)) + throw; + } + } + } + + // not found + return null; + } + + /// Get whether a file from the game's content folder exists. + /// The asset key. + private bool GetContentFolderFileExists(string key) + { + // get file path + string path = Path.Combine(this.GameContentManager.FullRootDirectory, key); + if (!path.EndsWith(".xnb")) + path += ".xnb"; + + // get file + return new FileInfo(path).Exists; + } } } -- cgit From c37fe62ca2ea8a072a16acd28bb38bc69911fd5b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 29 May 2019 23:17:13 -0400 Subject: no longer forward managed asset keys loaded through a mod content manager (#644) That isn't needed for any documented functionality, and allowed mods to load (and in some cases edit) a different mod's local assets. --- .../Framework/ContentManagers/ModContentManager.cs | 25 ++++++++-------------- 1 file changed, 9 insertions(+), 16 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 45303f6b..d6f077cb 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -59,26 +59,19 @@ namespace StardewModdingAPI.Framework.ContentManagers { assetName = this.AssertAndNormaliseAssetName(assetName); - // get managed asset - if (this.IsLoaded(assetName)) - return base.Load(assetName, language); + // resolve managed asset key if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { if (contentManagerID != this.Name) - { - T data = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, data, language); - return data; - } - - return this.LoadManagedAsset(assetName, contentManagerID, relativePath, language); + throw new SContentLoadException($"Can't load managed asset key '{assetName}' through content manager '{this.Name}' for a different mod."); + assetName = relativePath; } // get local asset string internalKey = this.GetInternalAssetKey(assetName); if (this.IsLoaded(internalKey)) return base.Load(internalKey, language); - return this.LoadManagedAsset(internalKey, this.Name, assetName, this.Language); + return this.LoadImpl(internalKey, this.Name, assetName, this.Language); } /// Create a new content manager for temporary use. @@ -108,13 +101,13 @@ namespace StardewModdingAPI.Framework.ContentManagers return this.Cache.ContainsKey(normalisedAssetName); } - /// Load a managed mod asset. + /// Load a local mod asset. /// The type of asset to load. - /// The internal asset key. + /// The mod asset cache key to save. /// The unique name for the content manager which should load this asset. /// The relative path within the mod folder. /// The language code for which to load content. - private T LoadManagedAsset(string internalKey, string contentManagerID, string relativePath, LanguageCode language) + private T LoadImpl(string cacheKey, string contentManagerID, string relativePath, LanguageCode language) { SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{relativePath}' from {contentManagerID}: {reasonPhrase}"); try @@ -151,7 +144,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); texture = this.PremultiplyTransparency(texture); - this.Inject(internalKey, texture, language); + this.Inject(cacheKey, texture, language); return (T)(object)texture; } @@ -167,7 +160,7 @@ namespace StardewModdingAPI.Framework.ContentManagers this.FixCustomTilesheetPaths(map, relativeMapPath: relativePath); // inject map - this.Inject(internalKey, map, this.Language); + this.Inject(cacheKey, map, this.Language); return (T)(object)map; default: -- cgit From 202ba23dcc24e63541eb78a535182ec89650a8fa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 May 2019 16:42:16 -0400 Subject: ignore root content managers when handling managed asset keys (#644) --- src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 10 +++++----- src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 2 +- src/SMAPI/Framework/ContentManagers/IContentManager.cs | 4 ++-- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index f54262b8..3f7c2cee 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -51,8 +51,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The absolute path to the . public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - /// Whether this content manager is for a mod folder. - public bool IsModContentManager { get; } + /// Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder). + public bool IsNamespaced { get; } /********* @@ -67,8 +67,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates monitoring and logging. /// Simplifies access to private code. /// A callback to invoke when the content manager is being disposed. - /// Whether this content manager is for a mod folder. - protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) + /// Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder). + protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isNamespaced) : base(serviceProvider, rootDirectory, currentCulture) { // init @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.ContentManagers this.Cache = new ContentCache(this, reflection); this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.OnDisposing = onDisposing; - this.IsModContentManager = isModFolder; + this.IsNamespaced = isNamespaced; // get asset data this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 085982b6..90278c36 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A callback to invoke when the content manager is being disposed. /// A callback to invoke the first time *any* game content manager loads an asset. public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, Action onLoadingFirstAsset) - : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false) { this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); this.OnLoadingFirstAsset = onLoadingFirstAsset; diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 4c362b0a..2365789b 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -22,8 +22,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The absolute path to the . string FullRootDirectory { get; } - /// Whether this content manager is for a mod folder. - bool IsModContentManager { get; } + /// Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder). + bool IsNamespaced { get; } /********* diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index d6f077cb..064a7907 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke when the content manager is being disposed. public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing) - : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.JsonHelper = jsonHelper; -- cgit From b9dec734693f50b3b32977cd505f091a2c8b8382 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 May 2019 17:40:21 -0400 Subject: disable mod-level asset caching (#644) This fixes an issue where some asset references could be shared between content managers, causing changes to propagate unintentionally. --- .../ContentManagers/BaseContentManager.cs | 123 +++++++++++--------- .../ContentManagers/GameContentManager.cs | 120 ++++++++++++------- .../Framework/ContentManagers/IContentManager.cs | 20 +--- .../Framework/ContentManagers/ModContentManager.cs | 129 ++++++++++++--------- 4 files changed, 224 insertions(+), 168 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 3f7c2cee..fc558eb9 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.IO; using System.Linq; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; @@ -38,6 +37,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The language enum values indexed by locale code. protected IDictionary LanguageCodes { get; } + /// A list of disposable assets. + private readonly List> Disposables = new List>(); + /********* ** Accessors @@ -88,54 +90,32 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) { - return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); + return this.Load(assetName, this.Language, useCache: true); } - /// Load the base asset without localisation. + /// Load an asset that has been processed by the content pipeline. /// The type of asset to load. /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T LoadBase(string assetName) + /// The language code for which to load content. + public override T Load(string assetName, LanguageCode language) { - return this.Load(assetName, LanguageCode.en); + return this.Load(assetName, language, useCache: true); } - /// Inject an asset into the cache. - /// The type of asset to inject. + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - /// The language code for which to inject the asset. - public virtual void Inject(string assetName, T value, LanguageCode language) - { - assetName = this.AssertAndNormaliseAssetName(assetName); - this.Cache[assetName] = value; - } + /// The language code for which to load content. + /// Whether to read/write the loaded asset to the asset cache. + public abstract T Load(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); - /// Get a copy of the given asset if supported. - /// The asset type. - /// The asset to clone. - public T CloneIfPossible(T asset) + /// Load the base asset without localisation. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + [Obsolete("This method is implemented for the base game and should not be used directly. To load an asset from the underlying content manager directly, use " + nameof(BaseContentManager.RawLoad) + " instead.")] + public override T LoadBase(string assetName) { - switch (asset as object) - { - case Texture2D source: - { - int[] pixels = new int[source.Width * source.Height]; - source.GetData(pixels); - - Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); - clone.SetData(pixels); - return (T)(object)clone; - } - - case Dictionary source: - return (T)(object)new Dictionary(source); - - case Dictionary source: - return (T)(object)new Dictionary(source); - - default: - return asset; - } + return this.Load(assetName, LanguageCode.en, useCache: true); } /// Perform any cleanup needed when the locale changes. @@ -228,11 +208,28 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Whether the content manager is being disposed (rather than finalized). protected override void Dispose(bool isDisposing) { + // ignore if disposed if (this.IsDisposed) return; this.IsDisposed = true; + // dispose uncached assets + foreach (WeakReference reference in this.Disposables) + { + if (reference.TryGetTarget(out IDisposable disposable)) + { + try + { + disposable.Dispose(); + } + catch { /* ignore dispose errors */ } + } + } + this.Disposables.Clear(); + + // raise event this.OnDisposing(this); + base.Dispose(isDisposing); } @@ -249,23 +246,26 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// Get the locale codes (like ja-JP) used in asset keys. - private IDictionary GetKeyLocales() + /// Load an asset file directly from the underlying content manager. + /// The type of asset to load. + /// The normalised asset key. + /// Whether to read/write the loaded asset to the asset cache. + protected virtual T RawLoad(string assetName, bool useCache) { - // create locale => code map - IDictionary map = new Dictionary(); - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - map[code] = this.GetLocale(code); - - return map; + return useCache + ? base.LoadBase(assetName) + : base.ReadAsset(assetName, disposable => this.Disposables.Add(new WeakReference(disposable))); } - /// Get the asset name from a cache key. - /// The input cache key. - private string GetAssetName(string cacheKey) + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + /// The language code for which to inject the asset. + protected virtual void Inject(string assetName, T value, LanguageCode language) { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; + assetName = this.AssertAndNormaliseAssetName(assetName); + this.Cache[assetName] = value; } /// Parse a cache key into its component parts. @@ -298,5 +298,24 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Get whether an asset has already been loaded. /// The normalised asset name. protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName); + + /// Get the locale codes (like ja-JP) used in asset keys. + private IDictionary GetKeyLocales() + { + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 90278c36..ecabcaca 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; @@ -59,7 +60,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The type of asset to load. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The language code for which to load content. - public override T Load(string assetName, LanguageCode language) + /// Whether to read/write the loaded asset to the asset cache. + public override T Load(string assetName, LocalizedContentManager.LanguageCode language, bool useCache) { // raise first-load callback if (GameContentManager.IsFirstLoad) @@ -71,17 +73,18 @@ namespace StardewModdingAPI.Framework.ContentManagers // normalise asset name assetName = this.AssertAndNormaliseAssetName(assetName); if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) - return this.Load(newAssetName, newLanguage); + return this.Load(newAssetName, newLanguage, useCache); // get from cache - if (this.IsLoaded(assetName)) - return base.Load(assetName, language); + if (useCache && this.IsLoaded(assetName)) + return this.RawLoad(assetName, language, useCache: true); // get managed asset if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { - T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, managedAsset, language); + T managedAsset = this.Coordinator.LoadManagedAsset(assetName, contentManagerID, relativePath, language); + if (useCache) + this.Inject(assetName, managedAsset, language); return managedAsset; } @@ -91,7 +94,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load(assetName, language); + data = this.RawLoad(assetName, language, useCache); } else { @@ -101,7 +104,7 @@ namespace StardewModdingAPI.Framework.ContentManagers IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName); IAssetData asset = this.ApplyLoader(info) - ?? new AssetDataForObject(info, base.Load(assetName, language), this.AssertAndNormaliseAssetName); + ?? new AssetDataForObject(info, this.RawLoad(assetName, language, useCache), this.AssertAndNormaliseAssetName); asset = this.ApplyEditors(info, asset); return (T)asset.Data; }); @@ -112,39 +115,6 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - /// The language code for which to inject the asset. - public override void Inject(string assetName, T value, LanguageCode language) - { - // handle explicit language in asset name - { - if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) - { - this.Inject(newAssetName, value, newLanguage); - return; - } - } - base.Inject(assetName, value, language); - - // track whether the injected asset is translatable for is-loaded lookups - string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; - if (this.Cache.ContainsKey(keyWithLocale)) - { - this.IsLocalisableLookup[assetName] = true; - this.IsLocalisableLookup[keyWithLocale] = true; - } - else if (this.Cache.ContainsKey(assetName)) - { - this.IsLocalisableLookup[assetName] = false; - this.IsLocalisableLookup[keyWithLocale] = false; - } - else - this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); - } - /// Perform any cleanup needed when the locale changes. public override void OnLocaleChanged() { @@ -199,6 +169,72 @@ namespace StardewModdingAPI.Framework.ContentManagers return false; } + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + /// The language code for which to inject the asset. + protected override void Inject(string assetName, T value, LanguageCode language) + { + // handle explicit language in asset name + { + if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) + { + this.Inject(newAssetName, value, newLanguage); + return; + } + } + base.Inject(assetName, value, language); + + // track whether the injected asset is translatable for is-loaded lookups + string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; + if (this.Cache.ContainsKey(keyWithLocale)) + { + this.IsLocalisableLookup[assetName] = true; + this.IsLocalisableLookup[keyWithLocale] = true; + } + else if (this.Cache.ContainsKey(assetName)) + { + this.IsLocalisableLookup[assetName] = false; + this.IsLocalisableLookup[keyWithLocale] = false; + } + else + this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); + } + + /// Load an asset file directly from the underlying content manager. + /// The type of asset to load. + /// The normalised asset key. + /// The language code for which to load content. + /// Whether to read/write the loaded asset to the asset cache. + /// Derived from . + private T RawLoad(string assetName, LanguageCode language, bool useCache) + { + // try translated asset + if (language != LocalizedContentManager.LanguageCode.en) + { + string translatedKey = $"{assetName}.{this.GetLocale(language)}"; + if (!this.IsLocalisableLookup.TryGetValue(translatedKey, out bool isTranslatable) || isTranslatable) + { + try + { +