summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ContentManagers
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-11-24 13:49:30 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-11-24 13:49:30 -0500
commita3f21685049cabf2d824c8060dc0b1de47e9449e (patch)
treead9add30e9da2a50e0ea0245f1546b7378f0d282 /src/SMAPI/Framework/ContentManagers
parent6521df7b131924835eb797251c1e956fae0d6e13 (diff)
parent277bf082675b98b95bf6184fe3c7a45b969c7ac2 (diff)
downloadSMAPI-a3f21685049cabf2d824c8060dc0b1de47e9449e.tar.gz
SMAPI-a3f21685049cabf2d824c8060dc0b1de47e9449e.tar.bz2
SMAPI-a3f21685049cabf2d824c8060dc0b1de47e9449e.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ContentManagers')
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs168
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs178
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs36
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs330
4 files changed, 527 insertions, 185 deletions
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 7821e454..5283340e 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -38,6 +38,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
@@ -51,8 +54,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
- /// <summary>Whether this content manager is for a mod folder.</summary>
- public bool IsModContentManager { get; }
+ /// <summary>Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder).</summary>
+ public bool IsNamespaced { get; }
/*********
@@ -62,13 +65,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
/// <param name="serviceProvider">The service provider to use to locate services.</param>
/// <param name="rootDirectory">The root directory to search for content.</param>
- /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="currentCulture">The current culture for which to localize content.</param>
/// <param name="coordinator">The central coordinator which manages content managers.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- /// <param name="isModFolder">Whether this content manager is for a mod folder.</param>
- protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isModFolder)
+ /// <param name="isNamespaced">Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder).</param>
+ protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced)
: base(serviceProvider, rootDirectory, currentCulture)
{
// init
@@ -77,7 +80,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);
@@ -88,69 +91,50 @@ 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>
- public void Inject<T>(string assetName, T value)
- {
- 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 localization.</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>Normalise path separators in a file path. For asset keys, see <see cref="AssertAndNormaliseAssetName"/> instead.</summary>
- /// <param name="path">The file path to normalise.</param>
+ /// <summary>Perform any cleanup needed when the locale changes.</summary>
+ public virtual void OnLocaleChanged() { }
+
+ /// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary>
+ /// <param name="path">The file path to normalize.</param>
[Pure]
- public string NormalisePathSeparators(string path)
+ public string NormalizePathSeparators(string path)
{
- return this.Cache.NormalisePathSeparators(path);
+ return this.Cache.NormalizePathSeparators(path);
}
- /// <summary>Assert that the given key has a valid format and return a normalised form consistent with the underlying cache.</summary>
+ /// <summary>Assert that the given key has a valid format and return a normalized form consistent with the underlying cache.</summary>
/// <param name="assetName">The asset key to check.</param>
/// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception>
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
- public string AssertAndNormaliseAssetName(string assetName)
+ public string AssertAndNormalizeAssetName(string assetName)
{
// NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid
// throwing other types like ArgumentException here.
@@ -159,7 +143,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (assetName.Intersect(Path.GetInvalidPathChars()).Any())
throw new SContentLoadException("The asset key or local path contains invalid characters.");
- return this.Cache.NormaliseKey(assetName);
+ return this.Cache.NormalizeKey(assetName);
}
/****
@@ -182,8 +166,8 @@ 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 bool IsLoaded(string assetName)
{
- assetName = this.Cache.NormaliseKey(assetName);
- return this.IsNormalisedKeyLoaded(assetName);
+ assetName = this.Cache.NormalizeKey(assetName);
+ return this.IsNormalizedKeyLoaded(assetName);
}
/// <summary>Get the cached asset keys.</summary>
@@ -225,11 +209,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);
}
@@ -246,32 +247,40 @@ 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 normalized 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;
+ // track asset key
+ if (value is Texture2D texture)
+ texture.Name = assetName;
+
+ // cache asset
+ assetName = this.AssertAndNormalizeAssetName(assetName);
+ this.Cache[assetName] = value;
}
/// <summary>Parse a cache key into its component parts.</summary>
/// <param name="cacheKey">The input cache key.</param>
/// <param name="assetName">The original asset name.</param>
- /// <param name="localeCode">The asset locale code (or <c>null</c> if not localised).</param>
+ /// <param name="localeCode">The asset locale code (or <c>null</c> if not localized).</param>
protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode)
{
- // handle localised key
+ // handle localized key
if (!string.IsNullOrWhiteSpace(cacheKey))
{
int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture);
@@ -293,7 +302,26 @@ 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);
+ /// <param name="normalizedAssetName">The normalized asset name.</param>
+ protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName);
+
+ /// <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 ee940cc7..0b563555 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;
@@ -25,8 +26,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Interceptors which edit matching assets after they're loaded.</summary>
private IDictionary<IModMetadata, IList<IAssetEditor>> Editors => this.Coordinator.Editors;
- /// <summary>A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded.</summary>
- private readonly IDictionary<string, bool> IsLocalisableLookup;
+ /// <summary>A lookup which indicates whether the asset is localizable (i.e. the filename contains the locale), if previously loaded.</summary>
+ private readonly IDictionary<string, bool> IsLocalizableLookup;
+
+ /// <summary>Whether the next load is the first for any game content manager.</summary>
+ private static bool IsFirstLoad = true;
+
+ /// <summary>A callback to invoke the first time *any* game content manager loads an asset.</summary>
+ private readonly Action OnLoadingFirstAsset;
/*********
@@ -36,37 +43,48 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
/// <param name="serviceProvider">The service provider to use to locate services.</param>
/// <param name="rootDirectory">The root directory to search for content.</param>
- /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="currentCulture">The current culture for which to localize content.</param>
/// <param name="coordinator">The central coordinator which manages content managers.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false)
+ /// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param>
+ public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false)
{
- this.IsLocalisableLookup = reflection.GetField<IDictionary<string, bool>>(this, "_localizedAsset").GetValue();
+ this.IsLocalizableLookup = reflection.GetField<IDictionary<string, bool>>(this, "_localizedAsset").GetValue();
+ this.OnLoadingFirstAsset = onLoadingFirstAsset;
}
/// <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)
+ /// <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)
{
- // normalise asset name
- assetName = this.AssertAndNormaliseAssetName(assetName);
+ // raise first-load callback
+ if (GameContentManager.IsFirstLoad)
+ {
+ GameContentManager.IsFirstLoad = false;
+ this.OnLoadingFirstAsset();
+ }
+
+ // normalize asset name
+ assetName = this.AssertAndNormalizeAssetName(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);
+ T managedAsset = this.Coordinator.LoadManagedAsset<T>(contentManagerID, relativePath);
+ if (useCache)
+ this.Inject(assetName, managedAsset, language);
return managedAsset;
}
@@ -76,27 +94,50 @@ 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
{
data = this.AssetsBeingLoaded.Track(assetName, () =>
{
string locale = this.GetLocale(language);
- IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName);
+ IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName);
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.AssertAndNormalizeAssetName);
asset = this.ApplyEditors<T>(info, asset);
return (T)asset.Data;
});
}
// update cache & return data
- this.Inject(assetName, data);
+ this.Inject(assetName, data, language);
return data;
}
+ /// <summary>Perform any cleanup needed when the locale changes.</summary>
+ public override void OnLocaleChanged()
+ {
+ base.OnLocaleChanged();
+
+ // find assets for which a translatable version was loaded
+ HashSet<string> removeAssetNames = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
+ foreach (string key in this.IsLocalizableLookup.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);
+ }
+
/// <summary>Create a new content manager for temporary use.</summary>
public override LocalizedContentManager CreateTemporary()
{
@@ -108,30 +149,107 @@ namespace StardewModdingAPI.Framework.ContentManagers
** 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)
+ /// <param name="normalizedAssetName">The normalized asset name.</param>
+ protected override bool IsNormalizedKeyLoaded(string normalizedAssetName)
{
// default English
- if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalisedAssetName))
- return this.Cache.ContainsKey(normalisedAssetName);
+ if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalizedAssetName))
+ return this.Cache.ContainsKey(normalizedAssetName);
// translated
- string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}";
- if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable))
+ string keyWithLocale = $"{normalizedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}";
+ if (this.IsLocalizableLookup.TryGetValue(keyWithLocale, out bool localizable))
{
- return localisable
- ? this.Cache.ContainsKey(localeKey)
- : this.Cache.ContainsKey(normalisedAssetName);
+ return localizable
+ ? this.Cache.ContainsKey(keyWithLocale)
+ : this.Cache.ContainsKey(normalizedAssetName);
}
// not loaded yet
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;
+ }
+ }
+
+ // save to cache
+ // Note: even if the asset was loaded and cached right before this method was called,
+ // we need to fully re-inject it here for two reasons:
+ // 1. So we can look up an asset by its base or localized key (the game/XNA logic
+ // only caches by the most specific key).
+ // 2. Because a mod asset loader/editor may have changed the asset in a way that
+ // doesn't change the instance stored in the cache, e.g. using `asset.ReplaceWith`.
+ string keyWithLocale = $"{assetName}.{this.GetLocale(language)}";
+ base.Inject(assetName, value, language);
+ if (this.Cache.ContainsKey(keyWithLocale))
+ base.Inject(keyWithLocale, value, language);
+
+ // track whether the injected asset is translatable for is-loaded lookups
+ if (this.Cache.ContainsKey(keyWithLocale))
+ {
+ this.IsLocalizableLookup[assetName] = true;
+ this.IsLocalizableLookup[keyWithLocale] = true;
+ }
+ else if (this.Cache.ContainsKey(assetName))
+ {
+ this.IsLocalizableLookup[assetName] = false;
+ this.IsLocalizableLookup[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 normalized 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.IsLocalizableLookup.TryGetValue(translatedKey, out bool isTranslatable) || isTranslatable)
+ {
+ try
+ {
+ T obj = base.RawLoad<T>(translatedKey, useCache);
+ this.IsLocalizableLookup[assetName] = true;
+ this.IsLocalizableLookup[translatedKey] = true;
+ return obj;
+ }
+ catch (ContentLoadException)
+ {
+ this.IsLocalizableLookup[assetName] = false;
+ this.IsLocalizableLookup[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>
/// <param name="language">The language code removed from the asset name.</param>
+ /// <returns>Returns whether the asset key contains an explicit language and was successfully parsed.</returns>
private bool TryParseExplicitLanguageAssetKey(string rawAsset, out string assetName, out LanguageCode language)
{
if (string.IsNullOrWhiteSpace(rawAsset))
@@ -188,7 +306,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)
@@ -205,7 +323,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// return matched asset
- return new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName);
+ return new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
}
/// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary>
@@ -214,7 +332,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="asset">The loaded asset.</param>
private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset)
{
- IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName);
+ IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
// edit asset
foreach (var entry in this.GetInterceptors(this.Editors))
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index 17618edd..12c01352 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;
@@ -23,8 +22,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
string FullRootDirectory { get; }
- /// <summary>Whether this content manager is for a mod folder.</summary>
- bool IsModContentManager { get; }
+ /// <summary>Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder).</summary>
+ bool IsNamespaced { get; }
/*********
@@ -33,35 +32,22 @@ 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>
- void Inject<T>(string assetName, T value);
+ /// <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>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);
+ /// <summary>Perform any cleanup needed when the locale changes.</summary>
+ void OnLocaleChanged();
- /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="AssertAndNormaliseAssetName"/> instead.</summary>
- /// <param name="path">The file path to normalise.</param>
+ /// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary>
+ /// <param name="path">The file path to normalize.</param>
[Pure]
- string NormalisePathSeparators(string path);
+ string NormalizePathSeparators(string path);
- /// <summary>Assert that the given key has a valid format and return a normalised form consistent with the underlying cache.</summary>
+ /// <summary>Assert that the given key has a valid format and return a normalized form consistent with the underlying cache.</summary>
/// <param name="assetName">The asset key to check.</param>
/// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception>
- [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
- string AssertAndNormaliseAssetName(string assetName);
+ string AssertAndNormalizeAssetName(string assetName);
/// <summary>Get the current content locale.</summary>
string GetLocale();
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 2c50ec04..90b86179 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.Serialization;
+using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
+using xTile;
+using xTile.Format;
+using xTile.ObjectModel;
+using xTile.Tiles;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -19,84 +26,89 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>