summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-03-20 12:53:27 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-03-20 12:53:27 -0400
commita42926868ae5878ed59d6406ca085b587299ba07 (patch)
treeff351e5c4cec39a1294795fb52a518656479e451 /src/SMAPI
parentd96cec88e461806c4676c9280455da19f5c7af24 (diff)
downloadSMAPI-a42926868ae5878ed59d6406ca085b587299ba07.tar.gz
SMAPI-a42926868ae5878ed59d6406ca085b587299ba07.tar.bz2
SMAPI-a42926868ae5878ed59d6406ca085b587299ba07.zip
encapsulate editor/loader operations (#766)
These will be used by the new content API, and allow handling the old one the same way.
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/Content/AssetEditOperation.cs30
-rw-r--r--src/SMAPI/Framework/Content/AssetLoadOperation.cs30
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs127
3 files changed, 143 insertions, 44 deletions
diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs
new file mode 100644
index 00000000..fa189d44
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>An edit to apply to an asset when it's requested from the content pipeline.</summary>
+ internal class AssetEditOperation
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod applying the edit.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>Apply the edit to an asset.</summary>
+ public Action<IAssetData> ApplyEdit { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod applying the edit.</param>
+ /// <param name="applyEdit">Apply the edit to an asset.</param>
+ public AssetEditOperation(IModMetadata mod, Action<IAssetData> applyEdit)
+ {
+ this.Mod = mod;
+ this.ApplyEdit = applyEdit;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs
new file mode 100644
index 00000000..d773cadd
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>An operation which provides the initial instance of an asset when it's requested from the content pipeline.</summary>
+ internal class AssetLoadOperation
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod applying the edit.</summary>
+ public IModMetadata Mod { get; }
+
+ /// <summary>Load the initial value for an asset.</summary>
+ public Func<IAssetInfo, object> GetData { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod applying the edit.</param>
+ /// <param name="getData">Load the initial value for an asset.</param>
+ public AssetLoadOperation(IModMetadata mod, Func<IAssetInfo, object> getData)
+ {
+ this.Mod = mod;
+ this.GetData = getData;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 9b8125ad..7ed1fcda 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -24,7 +24,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
** Fields
*********/
/// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary>
- private readonly ContextHash<string> AssetsBeingLoaded = new ContextHash<string>();
+ private readonly ContextHash<string> AssetsBeingLoaded = new();
/// <summary>Interceptors which provide the initial versions of matching assets.</summary>
private IList<ModLinked<IAssetLoader>> Loaders => this.Coordinator.Loaders;
@@ -79,12 +79,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
// custom asset from a loader
string locale = this.GetLocale();
IAssetInfo info = new AssetInfo(locale, assetName, typeof(object), this.AssertAndNormalizeAssetName);
- ModLinked<IAssetLoader>[] loaders = this.GetLoaders<object>(info).ToArray();
- if (loaders.Length > 1)
- {
- string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray();
- this.Monitor.Log($"Multiple mods want to provide the '{info.Name}' 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);
- }
+ AssetLoadOperation[] loaders = this.GetLoaders<object>(info).ToArray();
+
+ if (!this.AssertMaxOneLoader(info, loaders, out string error))
+ this.Monitor.Log(error, LogLevel.Warn);
return loaders.Length == 1;
}
@@ -261,7 +259,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// try base asset
return base.RawLoad<T>(assetName.Name, useCache);
}
- catch (ContentLoadException ex) when (ex.InnerException is FileNotFoundException innerEx && innerEx.InnerException == null)
+ 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.");
}
@@ -272,27 +270,31 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
private IAssetData ApplyLoader<T>(IAssetInfo info)
{
- // find matching loaders
- var loaders = this.GetLoaders<T>(info).ToArray();
-
- // validate loaders
- if (!loaders.Any())
- return null;
- if (loaders.Length > 1)
+ // find matching loader
+ AssetLoadOperation loader;
{
- string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray();
- this.Monitor.Log($"Multiple mods want to provide the '{info.Name}' 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;
+ AssetLoadOperation[] loaders = this.GetLoaders<T>(info).ToArray();
+
+ if (!this.AssertMaxOneLoader(info, loaders, out string error))
+ {
+ this.Monitor.Log(error, LogLevel.Warn);
+ return null;
+ }
+
+ loader = loaders.FirstOrDefault();
}
+ // no loader found
+ if (loader == null)
+ return null;
+
// fetch asset from loader
- IModMetadata mod = loaders[0].Mod;
- IAssetLoader loader = loaders[0].Data;
+ IModMetadata mod = loader.Mod;
T data;
try
{
- data = loader.Load<T>(info);
- this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'.", LogLevel.Trace);
+ data = (T)loader.GetData(info);
+ this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'.");
}
catch (Exception ex)
{
@@ -322,34 +324,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary<,>) || actualOpenType == typeof(List<>) || actualType == typeof(Texture2D) || actualType == typeof(Map)))
{
return (IAssetData)this.GetType()
- .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)
+ .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)!
.MakeGenericMethod(actualType)
.Invoke(this, new object[] { info, asset });
}
}
// edit asset
- foreach (var entry in this.Editors)
+ AssetEditOperation[] editors = this.GetEditors<T>(info).ToArray();
+ foreach (AssetEditOperation editor in editors)
{
- // check for match
- IModMetadata mod = entry.Mod;
- IAssetEditor editor = entry.Data;
- try
- {
- if (!editor.CanEdit<T>(info))
- continue;
- }
- catch (Exception ex)
- {
- mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
+ IModMetadata mod = editor.Mod;
// try edit
object prevAsset = asset.Data;
try
{
- editor.Edit<T>(asset);
+ editor.ApplyEdit(asset);
this.Monitor.Log($"{mod.DisplayName} edited {info.Name}.");
}
catch (Exception ex)
@@ -374,24 +365,72 @@ namespace StardewModdingAPI.Framework.ContentManagers
return asset;
}
- /// <summary>Get the asset loaders which handle the asset.</summary>
+ /// <summary>Get the asset loaders which handle an asset.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
- private IEnumerable<ModLinked<IAssetLoader>> GetLoaders<T>(IAssetInfo info)
+ private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info)
{
return this.Loaders
- .Where(entry =>
+ .Where(loader =>
{
try
{
- return entry.Data.CanLoad<T>(info);
+ return loader.Data.CanLoad<T>(info);
}
catch (Exception ex)
{
- entry.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 '{info.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return false;
}
- });
+ })
+ .Select(
+ loader => new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load<T>(assetInfo))
+ );
+ }
+
+ /// <summary>Get the asset editors to apply to an asset.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="info">The basic asset metadata.</param>
+ private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info)
+ {
+ return this.Editors
+ .Where(editor =>
+ {
+ try
+ {
+ return editor.Data.CanEdit<T>(info);
+ }
+ 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);
+ return false;
+ }
+ })
+ .Select(
+ editor => new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit<T>(assetData))
+ );
+ }
+
+ /// <summary>Assert that at most one loader will be applied to an asset.</summary>
+ /// <param name="info">The basic asset metadata.</param>
+ /// <param name="loaders">The asset loaders to apply.</param>
+ /// <param name="error">The error message to show to the user, if the method returns false.</param>
+ /// <returns>Returns true if only one loader will apply, else false.</returns>
+ private bool AssertMaxOneLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error)
+ {
+ if (loaders.Length <= 1)
+ {
+ error = null;
+ return true;
+ }
+
+ string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray();
+ string errorPhrase = loaderNames.Length > 1
+ ? $"Multiple mods want to provide '{info.Name}' asset ({string.Join(", ", loaderNames)})"
+ : $"The '{loaderNames[0]}' mod wants to provide the '{info.Name}' asset multiple times";
+
+ error = $"{errorPhrase}, 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.)";
+ return false;
}
/// <summary>Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible.</summary>