add support for asset loaders (#255)
This commit is contained in:
parent
600ef56286
commit
f95c7e8d72
|
@ -18,6 +18,13 @@ namespace StardewModdingAPI.Framework.Content
|
|||
public AssetDataForObject(string locale, string assetName, object data, Func<string, string> getNormalisedPath)
|
||||
: base(locale, assetName, data, getNormalisedPath) { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="info">The asset metadata.</param>
|
||||
/// <param name="data">The content data being read.</param>
|
||||
/// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
|
||||
public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalisedPath)
|
||||
: this(info.Locale, info.AssetName, data, getNormalisedPath) { }
|
||||
|
||||
/// <summary>Get a helper to manipulate the data as a dictionary.</summary>
|
||||
/// <typeparam name="TKey">The expected dictionary key.</typeparam>
|
||||
/// <typeparam name="TValue">The expected dictionary balue.</typeparam>
|
||||
|
|
|
@ -40,7 +40,13 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary>
|
||||
internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>();
|
||||
|
||||
/// <summary>Editors which change content assets after they're loaded.</summary>
|
||||
/// <summary>The observable implementation of <see cref="AssetLoaders"/>.</summary>
|
||||
internal ObservableCollection<IAssetLoader> ObservableAssetLoaders { get; } = new ObservableCollection<IAssetLoader>();
|
||||
|
||||
/// <summary>Interceptors which provide the initial versions of matching content assets.</summary>
|
||||
internal IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders;
|
||||
|
||||
/// <summary>Interceptors which edit matching content assets after they're loaded.</summary>
|
||||
internal IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors;
|
||||
|
||||
|
||||
|
|
|
@ -44,7 +44,10 @@ namespace StardewModdingAPI.Framework
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Implementations which change assets after they're loaded.</summary>
|
||||
/// <summary>Interceptors which provide the initial versions of matching assets.</summary>
|
||||
internal IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>();
|
||||
|
||||
/// <summary>Interceptors which edit matching assets after they're loaded.</summary>
|
||||
internal IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>();
|
||||
|
||||
/// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
|
||||
|
@ -126,9 +129,17 @@ namespace StardewModdingAPI.Framework
|
|||
return base.Load<T>(assetName);
|
||||
|
||||
// load asset
|
||||
T asset = this.GetAssetWithInterceptors(this.GetLocale(), assetName, () => base.Load<T>(assetName));
|
||||
this.Cache[assetName] = asset;
|
||||
return asset;
|
||||
T data;
|
||||
{
|
||||
IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName);
|
||||
IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName);
|
||||
asset = this.ApplyEditors<T>(info, asset);
|
||||
data = (T)asset.Data;
|
||||
}
|
||||
|
||||
// update cache & return data
|
||||
this.Cache[assetName] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Inject an asset into the cache.</summary>
|
||||
|
@ -198,6 +209,7 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.player.FarmerRenderer = new FarmerRenderer(this.Load<Texture2D>($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base"));
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
|
@ -209,73 +221,132 @@ namespace StardewModdingAPI.Framework
|
|||
|| this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset
|
||||
}
|
||||
|
||||
/// <summary>Read an asset with support for asset interceptors.</summary>
|
||||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="locale">The current content locale.</param>
|
||||
/// <param name="normalisedKey">The normalised asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
|
||||
/// <param name="getData">Get the asset from the underlying content manager.</param>
|
||||
private T GetAssetWithInterceptors<T>(string locale, string normalisedKey, Func<T> getData)
|
||||
/// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
||||
private IAssetData ApplyLoader<T>(IAssetInfo info)
|
||||
{
|
||||
// get metadata
|
||||
IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName);
|
||||
// find matching loaders
|
||||
var loaders = this.GetInterceptors(this.Loaders)
|
||||
.Where(entry =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return entry.Interceptor.CanLoad<T>(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
// validate loaders
|
||||
if (!loaders.Any())
|
||||
return null;
|
||||
if (loaders.Length > 1)
|
||||
{
|
||||
string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray();
|
||||
this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn);
|
||||
return null;
|
||||
}
|
||||
|
||||
// fetch asset from loader
|
||||
IModMetadata mod = loaders[0].Mod;
|
||||
IAssetLoader loader = loaders[0].Interceptor;
|
||||
T data;
|
||||
try
|
||||
{
|
||||
data = loader.Load<T>(info);
|
||||
this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// validate asset
|
||||
if (data == null)
|
||||
{
|
||||
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// return matched asset
|
||||
return new AssetDataForObject(info, data, this.NormaliseAssetName);
|
||||
}
|
||||
|
||||
/// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary>
|
||||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
/// <param name="asset">The loaded asset.</param>
|
||||
private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset)
|
||||
{
|
||||
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName);
|
||||
|
||||
// edit asset
|
||||
IAssetData data = this.GetAssetData(info, getData());
|
||||
foreach (var entry in this.GetAssetEditors())
|
||||
foreach (var entry in this.GetInterceptors(this.Editors))
|
||||
{
|
||||
// check for match
|
||||
IModMetadata mod = entry.Mod;
|
||||
IAssetEditor editor = entry.Editor;
|
||||
if (!editor.CanEdit<T>(info))
|
||||
IAssetEditor editor = entry.Interceptor;
|
||||
try
|
||||
{
|
||||
if (!editor.CanEdit<T>(info))
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
// try edit
|
||||
this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace);
|
||||
object prevAsset = data.Data;
|
||||
editor.Edit<T>(data);
|
||||
object prevAsset = asset.Data;
|
||||
try
|
||||
{
|
||||
editor.Edit<T>(asset);
|
||||
this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"{entry.Mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// validate edit
|
||||
if (data.Data == null)
|
||||
if (asset.Data == null)
|
||||
{
|
||||
data = this.GetAssetData(info, prevAsset);
|
||||
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value; ignoring override.", LogLevel.Warn);
|
||||
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn);
|
||||
asset = GetNewData(prevAsset);
|
||||
}
|
||||
else if (!(data.Data is T))
|
||||
else if (!(asset.Data is T))
|
||||
{
|
||||
data = this.GetAssetData(info, prevAsset);
|
||||
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn);
|
||||
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn);
|
||||
asset = GetNewData(prevAsset);
|
||||
}
|
||||
}
|
||||
|
||||
// return result
|
||||
return (T)data.Data;
|
||||
return asset;
|
||||
}
|
||||
|
||||
/// <summary>Get an asset edit helper.</summary>
|
||||
/// <param name="info">The asset info.</param>
|
||||
/// <param name="asset">The loaded asset data.</param>
|
||||
private IAssetData GetAssetData(IAssetInfo info, object asset)
|
||||
/// <summary>Get all registered interceptors from a list.</summary>
|
||||
private IEnumerable<(IModMetadata Mod, T Interceptor)> GetInterceptors<T>(IDictionary<IModMetadata, IList<T>> entries)
|
||||
{
|
||||
return new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName);
|
||||
}
|
||||
|
||||
/// <summary>Get all registered asset editors.</summary>
|
||||
private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors()
|
||||
{
|
||||
foreach (var entry in this.Editors)
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
IModMetadata metadata = entry.Key;
|
||||
IList<IAssetEditor> editors = entry.Value;
|
||||
IList<T> interceptors = entry.Value;
|
||||
|
||||
// special case if mod implements interface
|
||||
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||
if (metadata.Mod is IAssetEditor modAsEditor)
|
||||
yield return (metadata, modAsEditor);
|
||||
// special case if mod is an interceptor
|
||||
if (metadata.Mod is T modAsInterceptor)
|
||||
yield return (metadata, modAsInterceptor);
|
||||
|
||||
// registered editors
|
||||
foreach (IAssetEditor editor in editors)
|
||||
yield return (metadata, editor);
|
||||
foreach (T interceptor in interceptors)
|
||||
yield return (metadata, interceptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>Edits a loaded content asset.</summary>
|
||||
/// <summary>Edits matching content assets.</summary>
|
||||
public interface IAssetEditor
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use <see cref="IAssetEditor"/> instead.</summary>
|
||||
public interface IAssetLoader
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get whether this instance can load the initial version of the given asset.</summary>
|
||||
/// <param name="asset">Basic metadata about the asset being loaded.</param>
|
||||
bool CanLoad<T>(IAssetInfo asset);
|
||||
|
||||
/// <summary>Load a matched asset.</summary>
|
||||
/// <param name="asset">Basic metadata about the asset being loaded.</param>
|
||||
T Load<T>(IAssetInfo asset);
|
||||
}
|
||||
}
|
|
@ -708,7 +708,10 @@ namespace StardewModdingAPI
|
|||
{
|
||||
// add interceptors
|
||||
if (metadata.Mod.Helper.Content is ContentHelper helper)
|
||||
{
|
||||
this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors;
|
||||
this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders;
|
||||
}
|
||||
|
||||
// call entry method
|
||||
try
|
||||
|
@ -738,6 +741,11 @@ namespace StardewModdingAPI
|
|||
if (e.NewItems.Count > 0)
|
||||
this.ContentManager.Reset();
|
||||
};
|
||||
helper.ObservableAssetLoaders.CollectionChanged += (sender, e) =>
|
||||
{
|
||||
if (e.NewItems.Count > 0)
|
||||
this.ContentManager.Reset();
|
||||
};
|
||||
}
|
||||
}
|
||||
this.ContentManager.Reset();
|
||||
|
|
|
@ -159,6 +159,7 @@
|
|||
<Compile Include="Framework\TranslationHelper.cs" />
|
||||
<Compile Include="IAssetEditor.cs" />
|
||||
<Compile Include="IAssetInfo.cs" />
|
||||
<Compile Include="IAssetLoader.cs" />
|
||||
<Compile Include="ICommandHelper.cs" />
|
||||
<Compile Include="IAssetData.cs" />
|
||||
<Compile Include="IAssetDataForDictionary.cs" />
|
||||
|
|
Loading…
Reference in New Issue