summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs7
-rw-r--r--src/StardewModdingAPI/Framework/ContentHelper.cs8
-rw-r--r--src/StardewModdingAPI/Framework/SContentManager.cs159
-rw-r--r--src/StardewModdingAPI/IAssetEditor.cs2
-rw-r--r--src/StardewModdingAPI/IAssetLoader.cs17
-rw-r--r--src/StardewModdingAPI/Program.cs8
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.csproj1
7 files changed, 156 insertions, 46 deletions
diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs
index af2f54ae..f30003e4 100644
--- a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs
+++ b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs
@@ -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>
diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs
index b7773d6a..0c09fe94 100644
--- a/src/StardewModdingAPI/Framework/ContentHelper.cs
+++ b/src/StardewModdingAPI/Framework/ContentHelper.cs
@@ -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;
diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs
index 53afb729..0a8a0873 100644
--- a/src/StardewModdingAPI/Framework/SContentManager.cs
+++ b/src/StardewModdingAPI/Framework/SContentManager.cs
@@ -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;
- }
-
- /// <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)
- {
- return new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName);
+ return asset;
}
- /// <summary>Get all registered asset editors.</summary>
- private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors()
+ /// <summary>Get all registered interceptors from a list.</summary>
+ private IEnumerable<(IModMetadata Mod, T Interceptor)> GetInterceptors<T>(IDictionary<IModMetadata, IList<T>> entries)
{
- 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);
}
}
}
diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/StardewModdingAPI/IAssetEditor.cs
index b66ec15e..d2c6f295 100644
--- a/src/StardewModdingAPI/IAssetEditor.cs
+++ b/src/StardewModdingAPI/IAssetEditor.cs
@@ -1,6 +1,6 @@
namespace StardewModdingAPI
{
- /// <summary>Edits a loaded content asset.</summary>
+ /// <summary>Edits matching content assets.</summary>
public interface IAssetEditor
{
/*********
diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/StardewModdingAPI/IAssetLoader.cs
new file mode 100644
index 00000000..ad97b941
--- /dev/null
+++ b/src/StardewModdingAPI/IAssetLoader.cs
@@ -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);
+ }
+}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index 53efe1e3..483d2bc2 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -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();
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 1f2bd4bb..4d65b1af 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -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" />