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.
This commit is contained in:
parent
202ba23dcc
commit
b9dec73469
|
@ -20,7 +20,6 @@ These changes have not been released yet.
|
|||
* Fixed lag when a mod invalidates many NPC portraits/sprites at once.
|
||||
* Fixed map reloads resetting tilesheet seasons.
|
||||
* Fixed outdoor tilesheets being seasonalised when added to an indoor location.
|
||||
* Fixed mods able to directly load (and in some cases edit) a different mod's local assets using internal asset key forwarding.
|
||||
|
||||
* For modders:
|
||||
* Added support for content pack translations.
|
||||
|
@ -52,6 +51,8 @@ Released 13 September 2019 for Stardew Valley 1.3.36.
|
|||
* For modders:
|
||||
* `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. The change will only take effect when you recompile the mod.
|
||||
* Fixed 'location list changed' verbose log not correctly listing changes.
|
||||
* Fixed mods able to directly load (and in some cases edit) a different mod's local assets using internal asset key forwarding.
|
||||
* Fixed changes to a map loaded by a mod being persisted across content managers.
|
||||
|
||||
## 2.11.2
|
||||
Released 23 April 2019 for Stardew Valley 1.3.36.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 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)
|
||||
{
|
||||
return this.Load<T>(assetName, language, useCache: true);
|
||||
}
|
||||
|
||||
/// <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 abstract T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache);
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return this.Load<T>(assetName, LanguageCode.en);
|
||||
}
|
||||
|
||||
/// <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 virtual void Inject<T>(string assetName, T value, LanguageCode language)
|
||||
{
|
||||
assetName = this.AssertAndNormaliseAssetName(assetName);
|
||||
this.Cache[assetName] = value;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -29,28 +29,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <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();
|
||||
|
|
|
@ -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
|
||||
|
@ -51,27 +54,115 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
this.JsonHelper = jsonHelper;
|
||||
}
|
||||
|
||||
/// <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 Load<T>(string assetName)
|
||||
{
|
||||
return this.Load<T>(assetName, ModContentManager.NoLanguage, useCache: false);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return this.Load<T>(assetName, language, useCache: false);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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.");
|
||||
|
||||
// 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.");
|
||||
|
||||
// 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;
|
||||
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);
|
||||
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}");
|
||||
try
|
||||
{
|
||||
// get file
|
||||
FileInfo file = this.GetModFile(assetName);
|
||||
if (!file.Exists)
|
||||
throw GetContentError("the specified path doesn't exist.");
|
||||
|
||||
// load content
|
||||
switch (file.Extension.ToLower())
|
||||
{
|
||||
// XNB file
|
||||
case ".xnb":
|
||||
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;
|
||||
}
|
||||
|
||||
// unpacked image
|
||||
case ".png":
|
||||
// validate
|
||||
if (typeof(T) != typeof(Texture2D))
|
||||
throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
|
||||
|
||||
// fetch & cache
|
||||
using (FileStream stream = File.OpenRead(file.FullName))
|
||||
{
|
||||
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
|
||||
texture = this.PremultiplyTransparency(texture);
|
||||
return (T)(object)texture;
|
||||
}
|
||||
|
||||
// unpacked map
|
||||
case ".tbin":
|
||||
// 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: assetName);
|
||||
return (T)(object)map;
|
||||
|
||||
default:
|
||||
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!(ex is SContentLoadException))
|
||||
{
|
||||
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 '{assetName}' from {this.Name}.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Create a new content manager for temporary use.</summary>
|
||||
|
@ -101,80 +192,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
return this.Cache.ContainsKey(normalisedAssetName);
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
try
|
||||
{
|
||||
// get file
|
||||
FileInfo file = this.GetModFile(relativePath);
|
||||
if (!file.Exists)
|
||||
throw GetContentError("the specified path doesn't exist.");
|
||||
|
||||
// load content
|
||||
switch (file.Extension.ToLower())
|
||||
{
|
||||
// XNB file
|
||||
case ".xnb":
|
||||
return base.Load<T>(relativePath, language);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// unpacked image
|
||||
case ".png":
|
||||
// validate
|
||||
if (typeof(T) != typeof(Texture2D))
|
||||
throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
|
||||
|
||||
// fetch & cache
|
||||
using (FileStream stream = File.OpenRead(file.FullName))
|
||||
{
|
||||
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
|
||||
texture = this.PremultiplyTransparency(texture);
|
||||
this.Inject(cacheKey, texture, language);
|
||||
return (T)(object)texture;
|
||||
}
|
||||
|
||||
// unpacked map
|
||||
case ".tbin":
|
||||
// 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(cacheKey, map, this.Language);
|
||||
return (T)(object)map;
|
||||
|
||||
default:
|
||||
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!(ex is SContentLoadException))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
|
|
@ -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}'.");
|
||||
|
|
Loading…
Reference in New Issue