diff --git a/docs/release-notes.md b/docs/release-notes.md
index 5379950e..dada7726 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -19,9 +19,11 @@
* For modders:
* Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves).
+ * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on.
* Removed invalid-schedule validation which had false positives.
* Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder.
* Fixed dialogue asset changes not correctly propagated until the next day.
+ * Fixed issue where a mod which implemented `IAssetEditor`/`IAssetLoader` on its entry class could then remove itself from the editor/loader list.
* For SMAPI/tool developers:
* Added internal performance monitoring (thanks to Drachenkätzchen!). This is disabled by default in the current version, but can be enabled using the `performance` console command.
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index b60483f1..2fd31263 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -65,10 +65,10 @@ namespace StardewModdingAPI.Framework
public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language;
/// Interceptors which provide the initial versions of matching assets.
- public IDictionary> Loaders { get; } = new Dictionary>();
+ public IList> Loaders { get; } = new List>();
/// Interceptors which edit matching assets after they're loaded.
- public IDictionary> Editors { get; } = new Dictionary>();
+ public IList> Editors { get; } = new List>();
/// The absolute path to the .
public string FullRootDirectory { get; }
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 8930267d..eecdda74 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
private readonly ContextHash AssetsBeingLoaded = new ContextHash();
/// Interceptors which provide the initial versions of matching assets.
- private IDictionary> Loaders => this.Coordinator.Loaders;
+ private IList> Loaders => this.Coordinator.Loaders;
/// Interceptors which edit matching assets after they're loaded.
- private IDictionary> Editors => this.Coordinator.Editors;
+ private IList> Editors => this.Coordinator.Editors;
/// A lookup which indicates whether the asset is localizable (i.e. the filename contains the locale), if previously loaded.
private readonly IDictionary IsLocalizableLookup;
@@ -278,16 +278,16 @@ namespace StardewModdingAPI.Framework.ContentManagers
private IAssetData ApplyLoader(IAssetInfo info)
{
// find matching loaders
- var loaders = this.GetInterceptors(this.Loaders)
+ var loaders = this.Loaders
.Where(entry =>
{
try
{
- return entry.Value.CanLoad(info);
+ return entry.Data.CanLoad(info);
}
catch (Exception ex)
{
- entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
+ entry.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return false;
}
})
@@ -298,14 +298,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
return null;
if (loaders.Length > 1)
{
- string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray();
+ 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].Key;
- IAssetLoader loader = loaders[0].Value;
+ IModMetadata mod = loaders[0].Mod;
+ IAssetLoader loader = loaders[0].Data;
T data;
try
{
@@ -338,11 +338,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
// edit asset
- foreach (var entry in this.GetInterceptors(this.Editors))
+ foreach (var entry in this.Editors)
{
// check for match
- IModMetadata mod = entry.Key;
- IAssetEditor editor = entry.Value;
+ IModMetadata mod = entry.Mod;
+ IAssetEditor editor = entry.Data;
try
{
if (!editor.CanEdit(info))
@@ -382,19 +382,5 @@ namespace StardewModdingAPI.Framework.ContentManagers
// return result
return asset;
}
-
- /// Get all registered interceptors from a list.
- private IEnumerable> GetInterceptors(IDictionary> entries)
- {
- foreach (var entry in entries)
- {
- IModMetadata mod = entry.Key;
- IList interceptors = entry.Value;
-
- // registered editors
- foreach (T interceptor in interceptors)
- yield return new KeyValuePair(mod, interceptor);
- }
- }
}
}
diff --git a/src/SMAPI/Framework/ModLinked.cs b/src/SMAPI/Framework/ModLinked.cs
new file mode 100644
index 00000000..8cfe6f5f
--- /dev/null
+++ b/src/SMAPI/Framework/ModLinked.cs
@@ -0,0 +1,29 @@
+namespace StardewModdingAPI.Framework
+{
+ /// A generic tuple which links something to a mod.
+ /// The interceptor type.
+ internal class ModLinked
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The mod metadata.
+ public IModMetadata Mod { get; }
+
+ /// The instance linked to the mod.
+ public T Data { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The mod metadata.
+ /// The instance linked to the mod.
+ public ModLinked(IModMetadata mod, T data)
+ {
+ this.Mod = mod;
+ this.Data = data;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 9139b371..7e1f8770 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -807,13 +807,13 @@ namespace StardewModdingAPI.Framework
{
// ReSharper disable SuspiciousTypeConversion.Global
if (metadata.Mod is IAssetEditor editor)
- helper.ObservableAssetEditors.Add(editor);
+ this.ContentCore.Editors.Add(new ModLinked(metadata, editor));
if (metadata.Mod is IAssetLoader loader)
- helper.ObservableAssetLoaders.Add(loader);
+ this.ContentCore.Loaders.Add(new ModLinked(metadata, loader));
// ReSharper restore SuspiciousTypeConversion.Global
- this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors;
- this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders;
+ helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors);
+ helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders);
}
// call entry method
@@ -862,6 +862,24 @@ namespace StardewModdingAPI.Framework
this.ModRegistry.AreAllModsInitialized = true;
}
+ /// Handle a mod adding or removing asset interceptors.
+ /// The asset interceptor type (one of or ).
+ /// The mod metadata.
+ /// The interceptors that were added.
+ /// The interceptors that were removed.
+ /// The list to update.
+ private void OnInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed, IList> list)
+ {
+ foreach (T interceptor in added ?? new T[0])
+ list.Add(new ModLinked(mod, interceptor));
+
+ foreach (T interceptor in removed ?? new T[0])
+ {
+ foreach (ModLinked entry in list.Where(p => p.Mod == mod && object.ReferenceEquals(p.Data, interceptor)).ToArray())
+ list.Remove(entry);
+ }
+ }
+
/// Load a given mod.
/// The mod to load.
/// The mods being loaded.