diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 123 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 120 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/IContentManager.cs | 20 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 129 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 4 |
6 files changed, 229 insertions, 174 deletions
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 0a91ccd5..2dfe1f1c 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -176,16 +176,15 @@ namespace StardewModdingAPI.Framework /// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param> /// <param name="relativePath">The internal SMAPI asset key.</param> /// <param name="language">The language code for which to load content.</param> - public T LoadAndCloneManagedAsset<T>(string internalKey, string contentManagerID, string relativePath, LocalizedContentManager.LanguageCode language) + public T LoadManagedAsset<T>(string internalKey, string contentManagerID, string relativePath, LocalizedContentManager.LanguageCode language) { // get content manager IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); if (contentManager == null) throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); - // get cloned asset - T data = contentManager.Load<T>(internalKey, language); - return contentManager.CloneIfPossible(data); + // get fresh asset + return contentManager.Load<T>(relativePath, language, useCache: false); } /// <summary>Purge assets from the cache that match one of the interceptors.</summary> 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 /// <summary>The language enum values indexed by locale code.</summary> protected IDictionary<string, LanguageCode> LanguageCodes { get; } + /// <summary>A list of disposable assets.</summary> + private readonly List<WeakReference<IDisposable>> Disposables = new List<WeakReference<IDisposable>>(); + /********* ** Accessors @@ -88,54 +90,32 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> public override T Load<T>(string assetName) { - return this.Load<T>(assetName, LocalizedContentManager.CurrentLanguageCode); + return this.Load<T>(assetName, this.Language, useCache: true); } - /// <summary>Load the base asset without localisation.</summary> + /// <summary>Load an asset that has been processed by the content pipeline.</summary> /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - public override T LoadBase<T>(string assetName) + /// <param name="language">The language code for which to load content.</param> + public override T Load<T>(string assetName, LanguageCode language) { - return this.Load<T>(assetName, LanguageCode.en); + return this.Load<T>(assetName, language, useCache: true); } - /// <summary>Inject an asset into the cache.</summary> - /// <typeparam name="T">The type of asset to inject.</typeparam> + /// <summary>Load an asset that has been processed by the content pipeline.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="value">The asset value.</param> - /// <param name="language">The language code for which to inject the asset.</param> - public virtual void Inject<T>(string assetName, T value, LanguageCode language) - { - assetName = this.AssertAndNormaliseAssetName(assetName); - this.Cache[assetName] = value; - } + /// <param name="language">The language code for which to load content.</param> + /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + public abstract T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); - /// <summary>Get a copy of the given asset if supported.</summary> - /// <typeparam name="T">The asset type.</typeparam> - /// <param name="asset">The asset to clone.</param> - public T CloneIfPossible<T>(T asset) + /// <summary>Load the base asset without localisation.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + [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<T>(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<string, string> source: - return (T)(object)new Dictionary<string, string>(source); - - case Dictionary<int, string> source: - return (T)(object)new Dictionary<int, string>(source); - - default: - return asset; - } + return this.Load<T>(assetName, LanguageCode.en, useCache: true); } /// <summary>Perform any cleanup needed when the locale changes.</summary> @@ -228,11 +208,28 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="isDisposing">Whether the content manager is being disposed (rather than finalized).</param> protected override void Dispose(bool isDisposing) { + // ignore if disposed if (this.IsDisposed) return; this.IsDisposed = true; + // dispose uncached assets + foreach (WeakReference<IDisposable> 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 *********/ - /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> - private IDictionary<LanguageCode, string> GetKeyLocales() + /// <summary>Load an asset file directly from the underlying content manager.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The normalised asset key.</param> + /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + protected virtual T RawLoad<T>(string assetName, bool useCache) { - // create locale => code map - IDictionary<LanguageCode, string> map = new Dictionary<LanguageCode, string>(); - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - map[code] = this.GetLocale(code); - - return map; + return useCache + ? base.LoadBase<T>(assetName) + : base.ReadAsset<T>(assetName, disposable => this.Disposables.Add(new WeakReference<IDisposable>(disposable))); } - /// <summary>Get the asset name from a cache key.</summary> - /// <param name="cacheKey">The input cache key.</param> - private string GetAssetName(string cacheKey) + /// <summary>Inject an asset into the cache.</summary> + /// <typeparam name="T">The type of asset to inject.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="value">The asset value.</param> + /// <param name="language">The language code for which to inject the asset.</param> + protected virtual void Inject<T>(string assetName, T value, LanguageCode language) { - this.ParseCacheKey(cacheKey, out string assetName, out string _); - return assetName; + assetName = this.AssertAndNormaliseAssetName(assetName); + this.Cache[assetName] = value; } /// <summary>Parse a cache key into its component parts.</summary> @@ -298,5 +298,24 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Get whether an asset has already been loaded.</summary> /// <param name="normalisedAssetName">The normalised asset name.</param> protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName); + + /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> + private IDictionary<LanguageCode, string> GetKeyLocales() + { + // create locale => code map + IDictionary<LanguageCode, string> map = new Dictionary<LanguageCode, string>(); + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// <summary>Get the asset name from a cache key.</summary> + /// <param name="cacheKey">The input cache key.</param> + 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 /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> /// <param name="language">The language code for which to load content.</param> - public override T Load<T>(string assetName, LanguageCode language) + /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + public override T Load<T>(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<T>(newAssetName, newLanguage); + return this.Load<T>(newAssetName, newLanguage, useCache); // get from cache - if (this.IsLoaded(assetName)) - return base.Load<T>(assetName, language); + if (useCache && this.IsLoaded(assetName)) + return this.RawLoad<T>(assetName, language, useCache: true); // get managed asset if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { - T managedAsset = this.Coordinator.LoadAndCloneManagedAsset<T>(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, managedAsset, language); + T managedAsset = this.Coordinator.LoadManagedAsset<T>(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<T>(assetName, language); + data = this.RawLoad<T>(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<T>(info) - ?? new AssetDataForObject(info, base.Load<T>(assetName, language), this.AssertAndNormaliseAssetName); + ?? new AssetDataForObject(info, this.RawLoad<T>(assetName, language, useCache), this.AssertAndNormaliseAssetName); asset = this.ApplyEditors<T>(info, asset); return (T)asset.Data; }); @@ -112,39 +115,6 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } - /// <summary>Inject an asset into the cache.</summary> - /// <typeparam name="T">The type of asset to inject.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="value">The asset value.</param> - /// <param name="language">The language code for which to inject the asset.</param> - public override void Inject<T>(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); - } - /// <summary>Perform any cleanup needed when the locale changes.</summary> public override void OnLocaleChanged() { @@ -199,6 +169,72 @@ namespace StardewModdingAPI.Framework.ContentManagers return false; } + /// <summary>Inject an asset into the cache.</summary> + /// <typeparam name="T">The type of asset to inject.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="value">The asset value.</param> + /// <param name="language">The language code for which to inject the asset.</param> + protected override void Inject<T>(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); + } + + /// <summary>Load an asset file directly from the underlying content manager.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The normalised asset key.</param> + /// <param name="language">The language code for which to load content.</param> + /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + /// <remarks>Derived from <see cref="LocalizedContentManager.Load{T}(string, LocalizedContentManager.LanguageCode)"/>.</remarks> + private T RawLoad<T>(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 + { + T obj = base.RawLoad<T>(translatedKey, useCache); + this.IsLocalisableLookup[assetName] = true; + this.IsLocalisableLookup[translatedKey] = true; + return obj; + } + catch (ContentLoadException) + { + this.IsLocalisableLookup[assetName] = false; + this.IsLocalisableLookup[translatedKey] = false; + } + } + } + + // try base asset + return base.RawLoad<T>(assetName, useCache); + } + /// <summary>Parse an asset key that contains an explicit language into its asset name and language, if applicable.</summary> /// <param name="rawAsset">The asset key to parse.</param> /// <param name="assetName">The asset name without the language code.</param> @@ -260,7 +296,7 @@ namespace StardewModdingAPI.Framework.ContentManagers T data; try { - data = this.CloneIfPossible(loader.Load<T>(info)); + data = loader.Load<T>(info); this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); } catch (Exception ex) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 2365789b..78211821 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -32,25 +32,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Load an asset that has been processed by the content pipeline.</summary> /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - T Load<T>(string assetName); - - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> /// <param name="language">The language code for which to load content.</param> - T Load<T>(string assetName, LocalizedContentManager.LanguageCode language); - - /// <summary>Inject an asset into the cache.</summary> - /// <typeparam name="T">The type of asset to inject.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="value">The asset value.</param> - /// <param name="language">The language code for which to inject the asset.</param> - void Inject<T>(string assetName, T value, LocalizedContentManager.LanguageCode language); - - /// <summary>Get a copy of the given asset if supported.</summary> - /// <typeparam name="T">The asset type.</typeparam> - /// <param name="asset">The asset to clone.</param> - T CloneIfPossible<T>(T asset); + /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); /// <summary>Perform any cleanup needed when the locale changes.</summary> void OnLocaleChanged(); diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 064a7907..1d015138 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>The game content manager used for map tilesheets not provided by the mod.</summary> private readonly IContentManager GameContentManager; + /// <summary>The language code for language-agnostic mod assets.</summary> + private const LanguageCode NoLanguage = LanguageCode.en; + /********* ** Public methods @@ -54,66 +57,58 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Load an asset that has been processed by the content pipeline.</summary> /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language code for which to load content.</param> - public override T Load<T>(string assetName, LanguageCode language) + public override T Load<T>(string assetName) { - assetName = this.AssertAndNormaliseAssetName(assetName); - - // resolve managed asset key - if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) - { - if (contentManagerID != this.Name) - 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<T>(internalKey, language); - return this.LoadImpl<T>(internalKey, this.Name, assetName, this.Language); + return this.Load<T>(assetName, ModContentManager.NoLanguage, useCache: false); } - /// <summary>Create a new content manager for temporary use.</summary> - public override LocalizedContentManager CreateTemporary() + /// <summary>Load an asset that has been processed by the content pipeline.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="language">The language code for which to load content.</param> + public override T Load<T>(string assetName, LanguageCode language) { - throw new NotSupportedException("Can't create a temporary mod content manager."); + return this.Load<T>(assetName, language, useCache: false); } - /// <summary>Get the underlying key in the game's content cache for an asset. This does not validate whether the asset exists.</summary> - /// <param name="key">The local path to a content file relative to the mod folder.</param> - /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> - public string GetInternalAssetKey(string key) + /// <summary>Load an asset that has been processed by the content pipeline.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="language">The language code for which to load content.</param> + /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + public override T Load<T>(string assetName, LanguageCode language, bool useCache) { - FileInfo file = this.GetModFile(key); - string relativePath = PathUtilities.GetRelativePath(this.RootDirectory, file.FullName); - return Path.Combine(this.Name, relativePath); - } + assetName = this.AssertAndNormaliseAssetName(assetName); + // disable caching + // This is necessary to avoid assets being shared between content managers, which can + // cause changes to an asset through one content manager affecting the same asset in + // others (or even fresh content managers). See https://www.patreon.com/posts/27247161 + // for more background info. + if (useCache) + throw new InvalidOperationException("Mod content managers don't support asset caching."); - /********* - ** Private methods - *********/ - /// <summary>Get whether an asset has already been loaded.</summary> - /// <param name="normalisedAssetName">The normalised asset name.</param> - protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - return this.Cache.ContainsKey(normalisedAssetName); - } + // disable language handling + // Mod files don't support automatic translation logic, so this should never happen. + if (language != ModContentManager.NoLanguage) + throw new InvalidOperationException("Caching is not supported by the mod content manager."); - /// <summary>Load a local mod asset.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="cacheKey">The mod asset cache key to save.</param> - /// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param> - /// <param name="relativePath">The relative path within the mod folder.</param> - /// <param name="language">The language code for which to load content.</param> - private T LoadImpl<T>(string cacheKey, string contentManagerID, string relativePath, LanguageCode language) - { - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{relativePath}' from {contentManagerID}: {reasonPhrase}"); + // resolve managed asset key + { + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + if (contentManagerID != this.Name) + throw new SContentLoadException($"Can't load managed asset key '{assetName}' through content manager '{this.Name}' for a different mod."); + assetName = relativePath; + } + } + + // get local asset + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}"); try { // get file - FileInfo file = this.GetModFile(relativePath); + FileInfo file = this.GetModFile(assetName); if (!file.Exists) throw GetContentError("the specified path doesn't exist."); @@ -122,14 +117,13 @@ namespace StardewModdingAPI.Framework.ContentManagers { // XNB file case ".xnb": - return base.Load<T>(relativePath, language); + return this.RawLoad<T>(assetName, useCache: false); // unpacked data case ".json": { if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T data)) throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above - return data; } @@ -144,7 +138,6 @@ namespace StardewModdingAPI.Framework.ContentManagers { Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); texture = this.PremultiplyTransparency(texture); - this.Inject(cacheKey, texture, language); return (T)(object)texture; } @@ -157,10 +150,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - this.FixCustomTilesheetPaths(map, relativeMapPath: relativePath); - - // inject map - this.Inject(cacheKey, map, this.Language); + this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); return (T)(object)map; default: @@ -171,10 +161,37 @@ namespace StardewModdingAPI.Framework.ContentManagers { if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); - throw new SContentLoadException($"The content manager failed loading content asset '{relativePath}' from {contentManagerID}.", ex); + throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex); } } + /// <summary>Create a new content manager for temporary use.</summary> + public override LocalizedContentManager CreateTemporary() + { + throw new NotSupportedException("Can't create a temporary mod content manager."); + } + + /// <summary>Get the underlying key in the game's content cache for an asset. This does not validate whether the asset exists.</summary> + /// <param name="key">The local path to a content file relative to the mod folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + 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 + *********/ + /// <summary>Get whether an asset has already been loaded.</summary> + /// <param name="normalisedAssetName">The normalised asset name.</param> + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName); + } + /// <summary>Get a file from the mod folder.</summary> /// <param name="path">The asset path relative to the content folder.</param> private FileInfo GetModFile(string path) @@ -318,7 +335,7 @@ namespace StardewModdingAPI.Framework.ContentManagers try { - this.GameContentManager.Load<Texture2D>(contentKey); + this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset return contentKey; } catch diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 2d65f1f6..0be9aea5 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -91,10 +91,10 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.GameContentManager.Load<T>(key); + return this.GameContentManager.Load<T>(key, this.CurrentLocaleConstant, useCache: false); case ContentSource.ModFolder: - return this.ModContentManager.Load<T>(key); + return this.ModContentManager.Load<T>(key, this.CurrentLocaleConstant, useCache: false); default: throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); |