The game's content pipeline automatically loads localized variants if present. For example, it will try to load "Maps/cave.fr-FR", then "Maps/cave_international", then "Maps/cave". The old content API obfuscates this logic and treats them as interchangeable, which causes edge cases like bundle corruption (#812). This commit rewrites the loading logic to match the game logic when using the new content events, while maintaining the legacy behavior for the old IAssetLoader/IAssetEditor interfaces that'll be removed in SMAPI 4.0.0.
This commit is contained in:
parent
ad8912047b
commit
4c64f9f644
|
@ -10,6 +10,7 @@
|
||||||
* Added `--use-current-shell` to avoid opening a separate terminal window.
|
* Added `--use-current-shell` to avoid opening a separate terminal window.
|
||||||
* Fixed `--no-terminal` still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!).
|
* Fixed `--no-terminal` still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!).
|
||||||
* Fixed warning text when a mod causes an asset load conflict with itself.
|
* Fixed warning text when a mod causes an asset load conflict with itself.
|
||||||
|
* Fixed support for `_international` content assets (used in the movie theater).
|
||||||
|
|
||||||
* For mod authors:
|
* For mod authors:
|
||||||
* Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.
|
* Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.
|
||||||
|
|
|
@ -61,9 +61,6 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
|
/// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
|
||||||
private readonly List<IContentManager> ContentManagers = new();
|
private readonly List<IContentManager> ContentManagers = new();
|
||||||
|
|
||||||
/// <summary>The language code for language-agnostic mod assets.</summary>
|
|
||||||
private readonly LocalizedContentManager.LanguageCode DefaultLanguage = Constants.DefaultLanguage;
|
|
||||||
|
|
||||||
/// <summary>Whether the content coordinator has been disposed.</summary>
|
/// <summary>Whether the content coordinator has been disposed.</summary>
|
||||||
private bool IsDisposed;
|
private bool IsDisposed;
|
||||||
|
|
||||||
|
@ -350,7 +347,7 @@ namespace StardewModdingAPI.Framework
|
||||||
throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod.");
|
throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod.");
|
||||||
|
|
||||||
// get fresh asset
|
// get fresh asset
|
||||||
return contentManager.Load<T>(relativePath, this.DefaultLanguage, useCache: false);
|
return contentManager.LoadExact<T>(relativePath, useCache: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Purge matched assets from the cache.</summary>
|
/// <summary>Purge matched assets from the cache.</summary>
|
||||||
|
@ -467,9 +464,9 @@ namespace StardewModdingAPI.Framework
|
||||||
return this.ContentManagerLock.InReadLock(() =>
|
return this.ContentManagerLock.InReadLock(() =>
|
||||||
{
|
{
|
||||||
List<object> values = new List<object>();
|
List<object> values = new List<object>();
|
||||||
foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName, p.Language)))
|
foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName)))
|
||||||
{
|
{
|
||||||
object value = content.Load<object>(assetName, this.Language, useCache: true);
|
object value = content.LoadExact<object>(assetName, useCache: true);
|
||||||
values.Add(value);
|
values.Add(value);
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
|
@ -582,6 +579,8 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <param name="info">The asset info to load or edit.</param>
|
/// <param name="info">The asset info to load or edit.</param>
|
||||||
private IEnumerable<AssetOperationGroup> GetAssetOperationsWithoutCache<T>(IAssetInfo info)
|
private IEnumerable<AssetOperationGroup> GetAssetOperationsWithoutCache<T>(IAssetInfo info)
|
||||||
{
|
{
|
||||||
|
IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info);
|
||||||
|
|
||||||
// new content API
|
// new content API
|
||||||
foreach (AssetOperationGroup group in this.RequestAssetOperations(info))
|
foreach (AssetOperationGroup group in this.RequestAssetOperations(info))
|
||||||
yield return group;
|
yield return group;
|
||||||
|
@ -592,12 +591,12 @@ namespace StardewModdingAPI.Framework
|
||||||
// check if loader applies
|
// check if loader applies
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!loader.Data.CanLoad<T>(info))
|
if (!loader.Data.CanLoad<T>(legacyInfo))
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -610,7 +609,9 @@ namespace StardewModdingAPI.Framework
|
||||||
mod: loader.Mod,
|
mod: loader.Mod,
|
||||||
priority: AssetLoadPriority.Exclusive,
|
priority: AssetLoadPriority.Exclusive,
|
||||||
onBehalfOf: null,
|
onBehalfOf: null,
|
||||||
getData: assetInfo => loader.Data.Load<T>(assetInfo)
|
getData: assetInfo => loader.Data.Load<T>(
|
||||||
|
this.GetLegacyAssetInfo(assetInfo)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
editOperations: Array.Empty<AssetEditOperation>()
|
editOperations: Array.Empty<AssetEditOperation>()
|
||||||
|
@ -623,12 +624,12 @@ namespace StardewModdingAPI.Framework
|
||||||
// check if editor applies
|
// check if editor applies
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!editor.Data.CanEdit<T>(info))
|
if (!editor.Data.CanEdit<T>(legacyInfo))
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,11 +643,75 @@ namespace StardewModdingAPI.Framework
|
||||||
mod: editor.Mod,
|
mod: editor.Mod,
|
||||||
priority: AssetEditPriority.Default,
|
priority: AssetEditPriority.Default,
|
||||||
onBehalfOf: null,
|
onBehalfOf: null,
|
||||||
applyEdit: assetData => editor.Data.Edit<T>(assetData)
|
applyEdit: assetData => editor.Data.Edit<T>(
|
||||||
|
this.GetLegacyAssetData(assetData)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get an asset info compatible with legacy <see cref="IAssetLoader"/> and <see cref="IAssetEditor"/> instances, which always expect the base name.</summary>
|
||||||
|
/// <param name="asset">The asset info.</param>
|
||||||
|
private IAssetInfo GetLegacyAssetInfo(IAssetInfo asset)
|
||||||
|
{
|
||||||
|
if (!this.TryGetLegacyAssetName(asset.Name, out IAssetName legacyName))
|
||||||
|
return asset;
|
||||||
|
|
||||||
|
return new AssetInfo(
|
||||||
|
locale: null,
|
||||||
|
assetName: legacyName,
|
||||||
|
type: asset.DataType,
|
||||||
|
getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get an asset data compatible with legacy <see cref="IAssetLoader"/> and <see cref="IAssetEditor"/> instances, which always expect the base name.</summary>
|
||||||
|
/// <param name="asset">The asset data.</param>
|
||||||
|
private IAssetData GetLegacyAssetData(IAssetData asset)
|
||||||
|
{
|
||||||
|
if (!this.TryGetLegacyAssetName(asset.Name, out IAssetName legacyName))
|
||||||
|
return asset;
|
||||||
|
|
||||||
|
return asset.Name.LocaleCode == null
|
||||||
|
? asset
|
||||||
|
: new AssetDataForObject(
|
||||||
|
locale: null,
|
||||||
|
assetName: legacyName,
|
||||||
|
data: asset.Data,
|
||||||
|
getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get an asset name compatible with legacy <see cref="IAssetLoader"/> and <see cref="IAssetEditor"/> instances, which always expect the base name.</summary>
|
||||||
|
/// <param name="asset">The asset name to map.</param>
|
||||||
|
/// <param name="newAsset">The legacy asset name (or the <paramref name="asset"/> if no change is needed).</param>
|
||||||
|
/// <returns>Returns whether any change is needed for legacy compatibility.</returns>
|
||||||
|
private bool TryGetLegacyAssetName(IAssetName asset, out IAssetName newAsset)
|
||||||
|
{
|
||||||
|
// strip _international suffix
|
||||||
|
const string internationalSuffix = "_international";
|
||||||
|
if (asset.Name.EndsWith(internationalSuffix))
|
||||||
|
{
|
||||||
|
newAsset = new AssetName(
|
||||||
|
baseName: asset.Name[..^internationalSuffix.Length],
|
||||||
|
localeCode: null,
|
||||||
|
languageCode: null
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else strip locale
|
||||||
|
if (asset.LocaleCode != null)
|
||||||
|
{
|
||||||
|
newAsset = new AssetName(asset.BaseName, null, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else no change needed
|
||||||
|
newAsset = asset;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Diagnostics.Contracts;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.Xna.Framework.Content;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using StardewModdingAPI.Framework.Content;
|
using StardewModdingAPI.Framework.Content;
|
||||||
using StardewModdingAPI.Framework.Exceptions;
|
using StardewModdingAPI.Framework.Exceptions;
|
||||||
|
@ -32,6 +33,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/// <summary>Whether to enable more aggressive memory optimizations.</summary>
|
/// <summary>Whether to enable more aggressive memory optimizations.</summary>
|
||||||
protected readonly bool AggressiveMemoryOptimizations;
|
protected readonly bool AggressiveMemoryOptimizations;
|
||||||
|
|
||||||
|
/// <summary>Whether to automatically try resolving keys to a localized form if available.</summary>
|
||||||
|
protected bool TryLocalizeKeys = true;
|
||||||
|
|
||||||
/// <summary>Whether the content coordinator has been disposed.</summary>
|
/// <summary>Whether the content coordinator has been disposed.</summary>
|
||||||
private bool IsDisposed;
|
private bool IsDisposed;
|
||||||
|
|
||||||
|
@ -39,7 +43,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
private readonly Action<BaseContentManager> OnDisposing;
|
private readonly Action<BaseContentManager> OnDisposing;
|
||||||
|
|
||||||
/// <summary>A list of disposable assets.</summary>
|
/// <summary>A list of disposable assets.</summary>
|
||||||
private readonly List<WeakReference<IDisposable>> Disposables = new List<WeakReference<IDisposable>>();
|
private readonly List<WeakReference<IDisposable>> Disposables = new();
|
||||||
|
|
||||||
/// <summary>The disposable assets tracked by the base content manager.</summary>
|
/// <summary>The disposable assets tracked by the base content manager.</summary>
|
||||||
/// <remarks>This should be kept empty to avoid keeping disposable assets referenced forever, which prevents garbage collection when they're unused. Disposable assets are tracked by <see cref="Disposables"/> instead, which avoids a hard reference.</remarks>
|
/// <remarks>This should be kept empty to avoid keeping disposable assets referenced forever, which prevents garbage collection when they're unused. Disposable assets are tracked by <see cref="Disposables"/> instead, which avoids a hard reference.</remarks>
|
||||||
|
@ -115,11 +119,51 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
public override T Load<T>(string assetName, LanguageCode language)
|
public override T Load<T>(string assetName, LanguageCode language)
|
||||||
{
|
{
|
||||||
IAssetName parsedName = this.Coordinator.ParseAssetName(assetName);
|
IAssetName parsedName = this.Coordinator.ParseAssetName(assetName);
|
||||||
return this.Load<T>(parsedName, language, useCache: true);
|
return this.LoadLocalized<T>(parsedName, language, useCache: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract T Load<T>(IAssetName assetName, LanguageCode language, bool useCache);
|
public T LoadLocalized<T>(IAssetName assetName, LanguageCode language, bool useCache)
|
||||||
|
{
|
||||||
|
// ignore locale in English (or if disabled)
|
||||||
|
if (!this.TryLocalizeKeys || language == LocalizedContentManager.LanguageCode.en)
|
||||||
|
return this.LoadExact<T>(assetName, useCache: useCache);
|
||||||
|
|
||||||
|
// check for localized asset
|
||||||
|
if (!LocalizedContentManager.localizedAssetNames.TryGetValue(assetName.Name, out _))
|
||||||
|
{
|
||||||
|
string localeCode = this.LanguageCodeString(language);
|
||||||
|
IAssetName localizedName = new AssetName(baseName: assetName.BaseName, localeCode: localeCode, languageCode: language);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.LoadExact<T>(localizedName, useCache: useCache);
|
||||||
|
LocalizedContentManager.localizedAssetNames[assetName.Name] = localizedName.Name;
|
||||||
|
}
|
||||||
|
catch (ContentLoadException)
|
||||||
|
{
|
||||||
|
localizedName = new AssetName(assetName.BaseName + "_international", null, null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.LoadExact<T>(localizedName, useCache: useCache);
|
||||||
|
LocalizedContentManager.localizedAssetNames[assetName.Name] = localizedName.Name;
|
||||||
|
}
|
||||||
|
catch (ContentLoadException)
|
||||||
|
{
|
||||||
|
LocalizedContentManager.localizedAssetNames[assetName.Name] = assetName.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use cached key
|
||||||
|
string rawName = LocalizedContentManager.localizedAssetNames[assetName.Name];
|
||||||
|
if (assetName.Name != rawName)
|
||||||
|
assetName = this.Coordinator.ParseAssetName(assetName.Name);
|
||||||
|
return this.LoadExact<T>(assetName, useCache: useCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract T LoadExact<T>(IAssetName assetName, bool useCache);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void OnLocaleChanged() { }
|
public virtual void OnLocaleChanged() { }
|
||||||
|
@ -154,7 +198,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract bool IsLoaded(IAssetName assetName, LanguageCode language);
|
public bool IsLoaded(IAssetName assetName)
|
||||||
|
{
|
||||||
|
return this.Cache.ContainsKey(assetName.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Cache invalidation
|
** Cache invalidation
|
||||||
|
@ -241,26 +289,29 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/// <typeparam name="T">The type of asset to load.</typeparam>
|
/// <typeparam name="T">The type of asset to load.</typeparam>
|
||||||
/// <param name="assetName">The normalized asset key.</param>
|
/// <param name="assetName">The normalized asset key.</param>
|
||||||
/// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</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)
|
protected virtual T RawLoad<T>(IAssetName assetName, bool useCache)
|
||||||
{
|
{
|
||||||
return useCache
|
return useCache
|
||||||
? base.LoadBase<T>(assetName)
|
? base.LoadBase<T>(assetName.Name)
|
||||||
: base.ReadAsset<T>(assetName, disposable => this.Disposables.Add(new WeakReference<IDisposable>(disposable)));
|
: base.ReadAsset<T>(assetName.Name, disposable => this.Disposables.Add(new WeakReference<IDisposable>(disposable)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Add tracking data to an asset and add it to the cache.</summary>
|
/// <summary>Add tracking data to an asset and add it to the cache.</summary>
|
||||||
/// <typeparam name="T">The type of asset to inject.</typeparam>
|
/// <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="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="value">The asset value.</param>
|
||||||
/// <param name="language">The language code for which to inject the asset.</param>
|
|
||||||
/// <param name="useCache">Whether to save the asset to the asset cache.</param>
|
/// <param name="useCache">Whether to save the asset to the asset cache.</param>
|
||||||
protected virtual void TrackAsset<T>(IAssetName assetName, T value, LanguageCode language, bool useCache)
|
protected virtual void TrackAsset<T>(IAssetName assetName, T value, bool useCache)
|
||||||
{
|
{
|
||||||
// track asset key
|
// track asset key
|
||||||
if (value is Texture2D texture)
|
if (value is Texture2D texture)
|
||||||
texture.Name = assetName.Name;
|
texture.Name = assetName.Name;
|
||||||
|
|
||||||
// cache asset
|
// 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 because a mod editor may have changed the asset in a
|
||||||
|
// way that doesn't change the instance stored in the cache, e.g. using
|
||||||
|
// `asset.ReplaceWith`.
|
||||||
if (useCache)
|
if (useCache)
|
||||||
this.Cache[assetName.Name] = value;
|
this.Cache[assetName.Name] = value;
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,9 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Xna.Framework.Content;
|
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using StardewModdingAPI.Events;
|
using StardewModdingAPI.Events;
|
||||||
using StardewModdingAPI.Framework.Content;
|
using StardewModdingAPI.Framework.Content;
|
||||||
using StardewModdingAPI.Framework.Exceptions;
|
|
||||||
using StardewModdingAPI.Framework.Reflection;
|
using StardewModdingAPI.Framework.Reflection;
|
||||||
using StardewModdingAPI.Framework.Utilities;
|
using StardewModdingAPI.Framework.Utilities;
|
||||||
using StardewModdingAPI.Internal;
|
using StardewModdingAPI.Internal;
|
||||||
|
@ -91,7 +89,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T Load<T>(IAssetName assetName, LanguageCode language, bool useCache)
|
public override T LoadExact<T>(IAssetName assetName, bool useCache)
|
||||||
{
|
{
|
||||||
// raise first-load callback
|
// raise first-load callback
|
||||||
if (GameContentManager.IsFirstLoad)
|
if (GameContentManager.IsFirstLoad)
|
||||||
|
@ -100,19 +98,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
this.OnLoadingFirstAsset();
|
this.OnLoadingFirstAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalize asset name
|
|
||||||
if (assetName.LanguageCode.HasValue)
|
|
||||||
return this.Load<T>(this.Coordinator.ParseAssetName(assetName.BaseName), assetName.LanguageCode.Value, useCache);
|
|
||||||
|
|
||||||
// get from cache
|
// get from cache
|
||||||
if (useCache && this.IsLoaded(assetName, language))
|
if (useCache && this.IsLoaded(assetName))
|
||||||
return this.RawLoad<T>(assetName, language, useCache: true);
|
return this.RawLoad<T>(assetName, useCache: true);
|
||||||
|
|
||||||
// get managed asset
|
// get managed asset
|
||||||
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
|
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
|
||||||
{
|
{
|
||||||
T managedAsset = this.Coordinator.LoadManagedAsset<T>(contentManagerID, relativePath);
|
T managedAsset = this.Coordinator.LoadManagedAsset<T>(contentManagerID, relativePath);
|
||||||
this.TrackAsset(assetName, managedAsset, language, useCache);
|
this.TrackAsset(assetName, managedAsset, useCache);
|
||||||
return managedAsset;
|
return managedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,44 +116,29 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
{
|
{
|
||||||
this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn);
|
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}");
|
this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}");
|
||||||
data = this.RawLoad<T>(assetName, language, useCache);
|
data = this.RawLoad<T>(assetName, useCache);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
data = this.AssetsBeingLoaded.Track(assetName.Name, () =>
|
data = this.AssetsBeingLoaded.Track(assetName.Name, () =>
|
||||||
{
|
{
|
||||||
string locale = this.GetLocale(language);
|
IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName);
|
||||||
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName);
|
|
||||||
IAssetData asset =
|
IAssetData asset =
|
||||||
this.ApplyLoader<T>(info)
|
this.ApplyLoader<T>(info)
|
||||||
?? new AssetDataForObject(info, this.RawLoad<T>(assetName, language, useCache), this.AssertAndNormalizeAssetName);
|
?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName);
|
||||||
asset = this.ApplyEditors<T>(info, asset);
|
asset = this.ApplyEditors<T>(info, asset);
|
||||||
return (T)asset.Data;
|
return (T)asset.Data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// update cache
|
// update cache
|
||||||
this.TrackAsset(assetName, data, language, useCache);
|
this.TrackAsset(assetName, data, useCache);
|
||||||
|
|
||||||
// raise event & return data
|
// raise event & return data
|
||||||
this.OnAssetLoaded(this, assetName);
|
this.OnAssetLoaded(this, assetName);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool IsLoaded(IAssetName assetName, LanguageCode language)
|
|
||||||
{
|
|
||||||
string cachedKey = null;
|
|
||||||
bool localized =
|
|
||||||
language != LanguageCode.en
|
|
||||||
&& !this.Coordinator.IsManagedAssetKey(assetName)
|
|
||||||
&& this.LocalizedAssetNames.TryGetValue(assetName.Name, out cachedKey);
|
|
||||||
|
|
||||||
return localized
|
|
||||||
? this.Cache.ContainsKey(cachedKey)
|
|
||||||
: this.Cache.ContainsKey(assetName.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void OnLocaleChanged()
|
public override void OnLocaleChanged()
|
||||||
{
|
{
|
||||||
|
@ -175,7 +154,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
|
|
||||||
// invalidate translatable assets
|
// invalidate translatable assets
|
||||||
string[] invalidated = this
|
string[] invalidated = this
|
||||||
.InvalidateCache((key, type) =>
|
.InvalidateCache((key, _) =>
|
||||||
removeAssetNames.Contains(key)
|
removeAssetNames.Contains(key)
|
||||||
|| removeAssetNames.Contains(this.Coordinator.ParseAssetName(key).BaseName)
|
|| removeAssetNames.Contains(this.Coordinator.ParseAssetName(key).BaseName)
|
||||||
)
|
)
|
||||||
|
@ -196,81 +175,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** Private methods
|
||||||
*********/
|
*********/
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void TrackAsset<T>(IAssetName assetName, T value, LanguageCode language, bool useCache)
|
|
||||||
{
|
|
||||||
// handle explicit language in asset name
|
|
||||||
{
|
|
||||||
if (assetName.LanguageCode.HasValue)
|
|
||||||
{
|
|
||||||
this.TrackAsset(this.Coordinator.ParseAssetName(assetName.BaseName), value, assetName.LanguageCode.Value, useCache);
|
|
||||||
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`.
|
|
||||||
if (useCache)
|
|
||||||
{
|
|
||||||
IAssetName translatedKey = new AssetName(assetName.Name, this.GetLocale(language), language);
|
|
||||||
base.TrackAsset(assetName, value, language, useCache: true);
|
|
||||||
if (this.Cache.ContainsKey(translatedKey.Name))
|
|
||||||
base.TrackAsset(translatedKey, value, language, useCache: true);
|
|
||||||
|
|
||||||
// track whether the injected asset is translatable for is-loaded lookups
|
|
||||||
if (this.Cache.ContainsKey(translatedKey.Name))
|
|
||||||
this.LocalizedAssetNames[assetName.Name] = translatedKey.Name;
|
|
||||||
else if (this.Cache.ContainsKey(assetName.Name))
|
|
||||||
this.LocalizedAssetNames[assetName.Name] = assetName.Name;
|
|
||||||
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>(IAssetName assetName, LanguageCode language, bool useCache)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// use cached key
|
|
||||||
if (language == this.Language && this.LocalizedAssetNames.TryGetValue(assetName.Name, out string cachedKey))
|
|
||||||
return base.RawLoad<T>(cachedKey, useCache);
|
|
||||||
|
|
||||||
// try translated key
|
|
||||||
if (language != LanguageCode.en)
|
|
||||||
{
|
|
||||||
string translatedKey = $"{assetName}.{this.GetLocale(language)}";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
T obj = base.RawLoad<T>(translatedKey, useCache);
|
|
||||||
this.LocalizedAssetNames[assetName.Name] = translatedKey;
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
catch (ContentLoadException)
|
|
||||||
{
|
|
||||||
this.LocalizedAssetNames[assetName.Name] = assetName.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try base asset
|
|
||||||
return base.RawLoad<T>(assetName.Name, useCache);
|
|
||||||
}
|
|
||||||
catch (ContentLoadException ex) when (ex.InnerException is FileNotFoundException { InnerException: null })
|
|
||||||
{
|
|
||||||
throw new SContentLoadException($"Error loading \"{assetName}\": it isn't in the Content folder and no mod provided it.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Load the initial asset from the registered loaders.</summary>
|
/// <summary>Load the initial asset from the registered loaders.</summary>
|
||||||
/// <param name="info">The basic asset metadata.</param>
|
/// <param name="info">The basic asset metadata.</param>
|
||||||
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
||||||
|
|
|
@ -25,9 +25,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, onAssetLoaded, aggressiveMemoryOptimizations) { }
|
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, onAssetLoaded, aggressiveMemoryOptimizations) { }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T Load<T>(IAssetName assetName, LanguageCode language, bool useCache)
|
public override T LoadExact<T>(IAssetName assetName, bool useCache)
|
||||||
{
|
{
|
||||||
T data = base.Load<T>(assetName, language, useCache);
|
T data = base.LoadExact<T>(assetName, useCache);
|
||||||
|
|
||||||
if (data is Texture2D texture)
|
if (data is Texture2D texture)
|
||||||
texture.Tag = this.Tag;
|
texture.Tag = this.Tag;
|
||||||
|
|
|
@ -32,12 +32,17 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/// <param name="assetName">The normalized asset name.</param>
|
/// <param name="assetName">The normalized asset name.</param>
|
||||||
bool DoesAssetExist(IAssetName assetName);
|
bool DoesAssetExist(IAssetName assetName);
|
||||||
|
|
||||||
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
|
/// <summary>Load an asset through the content pipeline, using a localized variant of the <paramref name="assetName"/> if available.</summary>
|
||||||
/// <typeparam name="T">The type of asset to load.</typeparam>
|
/// <typeparam name="T">The type of asset to load.</typeparam>
|
||||||
/// <param name="assetName">The asset name relative to the loader root directory.</param>
|
/// <param name="assetName">The asset name relative to the loader root directory.</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>
|
/// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
|
||||||
T Load<T>(IAssetName assetName, LocalizedContentManager.LanguageCode language, bool useCache);
|
T LoadLocalized<T>(IAssetName assetName, LocalizedContentManager.LanguageCode language, bool useCache);
|
||||||
|
|
||||||
|
/// <summary>Load an asset through the content pipeline, using the exact asset name without checking for localized variants.</summary>
|
||||||
|
/// <typeparam name="T">The type of asset to load.</typeparam>
|
||||||
|
/// <param name="assetName">The asset name relative to the loader root directory.</param>
|
||||||
|
/// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
|
||||||
|
T LoadExact<T>(IAssetName assetName, bool useCache);
|
||||||
|
|
||||||
/// <summary>Assert that the given key has a valid format and return a normalized 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>
|
/// <param name="assetName">The asset key to check.</param>
|
||||||
|
@ -53,8 +58,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
|
|
||||||
/// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
|
/// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
|
||||||
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
/// <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.</param>
|
bool IsLoaded(IAssetName assetName);
|
||||||
bool IsLoaded(IAssetName assetName, LocalizedContentManager.LanguageCode language);
|
|
||||||
|
|
||||||
/// <summary>Purge matched assets from the cache.</summary>
|
/// <summary>Purge matched assets from the cache.</summary>
|
||||||
/// <param name="predicate">Matches the asset keys to invalidate.</param>
|
/// <param name="predicate">Matches the asset keys to invalidate.</param>
|
||||||
|
|
|
@ -61,6 +61,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
this.GameContentManager = gameContentManager;
|
this.GameContentManager = gameContentManager;
|
||||||
this.JsonHelper = jsonHelper;
|
this.JsonHelper = jsonHelper;
|
||||||
this.ModName = modName;
|
this.ModName = modName;
|
||||||
|
|
||||||
|
this.TryLocalizeKeys = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -80,14 +82,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T Load<T>(string assetName, LanguageCode language)
|
public override T LoadExact<T>(IAssetName assetName, bool useCache)
|
||||||
{
|
|
||||||
IAssetName parsedName = this.Coordinator.ParseAssetName(assetName);
|
|
||||||
return this.Load<T>(parsedName, language, useCache: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T Load<T>(IAssetName assetName, LanguageCode language, bool useCache)
|
|
||||||
{
|
{
|
||||||
// disable caching
|
// disable caching
|
||||||
// This is necessary to avoid assets being shared between content managers, which can
|
// This is necessary to avoid assets being shared between content managers, which can
|
||||||
|
@ -97,11 +92,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
if (useCache)
|
if (useCache)
|
||||||
throw new InvalidOperationException("Mod content managers don't support asset caching.");
|
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 != this.DefaultLanguage)
|
|
||||||
throw new InvalidOperationException("Localized assets aren't supported by the mod content manager.");
|
|
||||||
|
|
||||||
// resolve managed asset key
|
// resolve managed asset key
|
||||||
{
|
{
|
||||||
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
|
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
|
||||||
|
@ -130,14 +120,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
{
|
{
|
||||||
// the underlying content manager adds a .xnb extension implicitly, so
|
// the underlying content manager adds a .xnb extension implicitly, so
|
||||||
// we need to strip it here to avoid trying to load a '.xnb.xnb' file.
|
// we need to strip it here to avoid trying to load a '.xnb.xnb' file.
|
||||||
string loadName = assetName.Name[..^".xnb".Length];
|
IAssetName loadName = this.Coordinator.ParseAssetName(assetName.Name[..^".xnb".Length]);
|
||||||
|
|
||||||
// load asset
|
// load asset
|
||||||
asset = this.RawLoad<T>(loadName, useCache: false);
|
asset = this.RawLoad<T>(loadName, useCache: false);
|
||||||
if (asset is Map map)
|
if (asset is Map map)
|
||||||
{
|
{
|
||||||
map.assetPath = loadName;
|
map.assetPath = loadName.Name;
|
||||||
this.FixTilesheetPaths(map, relativeMapPath: loadName, fixEagerPathPrefixes: true);
|
this.FixTilesheetPaths(map, relativeMapPath: loadName.Name, fixEagerPathPrefixes: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -201,16 +191,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
}
|
}
|
||||||
|
|
||||||
// track & return asset
|
// track & return asset
|
||||||
this.TrackAsset(assetName, asset, language, useCache);
|
this.TrackAsset(assetName, asset, useCache);
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool IsLoaded(IAssetName assetName, LanguageCode language)
|
|
||||||
{
|
|
||||||
return this.Cache.ContainsKey(assetName.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override LocalizedContentManager CreateTemporary()
|
public override LocalizedContentManager CreateTemporary()
|
||||||
{
|
{
|
||||||
|
@ -371,7 +355,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
IAssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath));
|
IAssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
|
this.GameContentManager.LoadLocalized<Texture2D>(contentKey, this.GameContentManager.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
|
||||||
assetName = contentKey;
|
assetName = contentKey;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,10 +88,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
switch (source)
|
switch (source)
|
||||||
{
|
{
|
||||||
case ContentSource.GameContent:
|
case ContentSource.GameContent:
|
||||||
return this.GameContentManager.Load<T>(assetName, this.CurrentLocaleConstant, useCache: false);
|
return this.GameContentManager.LoadLocalized<T>(assetName, this.CurrentLocaleConstant, useCache: false);
|
||||||
|
|
||||||
case ContentSource.ModFolder:
|
case ContentSource.ModFolder:
|
||||||
return this.ModContentManager.Load<T>(assetName, Constants.DefaultLanguage, useCache: false);
|
return this.ModContentManager.LoadExact<T>(assetName, useCache: false);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
|
throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
|
||||||
|
|
Loading…
Reference in New Issue