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.