From 021891ff0ceb6b327bc196c336aa56ddfaf99b0e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 Mar 2022 22:49:14 -0400 Subject: add load conflict resolution option (#766) --- src/SMAPI/Events/AssetRequestedEventArgs.cs | 10 +++++++--- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 7 ++++++- src/SMAPI/Framework/ContentCoordinator.cs | 1 + .../Framework/ContentManagers/GameContentManager.cs | 17 ++++++++++------- 4 files changed, 24 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index 774ab808..9942079b 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -50,18 +50,20 @@ namespace StardewModdingAPI.Events /// Provide the initial instance for the asset, instead of trying to load it from the game's Content folder. /// Get the initial instance of an asset. /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. + /// When there are multiple loads that apply to the same asset, this indicates whether this one can be skipped to resolve the conflict. If all loads allow skipping, the first one that was registered will be applied. If this is false, SMAPI will raise an error and apply none of them. /// /// Usage notes: /// /// The asset doesn't need to exist in the game's Content folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder. - /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. + /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFrom(Func load, string onBehalfOf = null) + public void LoadFrom(Func load, string onBehalfOf = null, bool allowSkipOnConflict = false) { this.LoadOperations.Add( new AssetLoadOperation( mod: this.Mod, + allowSkipOnConflict: allowSkipOnConflict, onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), getData: _ => load() ) @@ -71,6 +73,7 @@ namespace StardewModdingAPI.Events /// Provide the initial instance for the asset from a file in your mod folder, instead of trying to load it from the game's Content folder. /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. /// The relative path to the file in your mod folder. + /// When there are multiple loads that apply to the same asset, this indicates whether this one can be skipped to resolve the conflict. If all loads allow skipping, the first one that was registered will be applied. If this is false, SMAPI will raise an error and apply none of them. /// /// Usage notes: /// @@ -78,11 +81,12 @@ namespace StardewModdingAPI.Events /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. /// /// - public void LoadFromModFile(string relativePath) + public void LoadFromModFile(string relativePath, bool allowSkipOnConflict = false) { this.LoadOperations.Add( new AssetLoadOperation( mod: this.Mod, + allowSkipOnConflict: allowSkipOnConflict, onBehalfOf: null, _ => this.Mod.Mod.Helper.Content.Load(relativePath)) ); diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index 29bf1518..36baf1aa 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI.Framework.Content /// The content pack on whose behalf the asset is being loaded, if any. public IModMetadata OnBehalfOf { get; } + /// Whether to allow skipping this operation to resolve a load conflict. + public bool AllowSkipOnConflict { get; } + /// Load the initial value for an asset. public Func GetData { get; } @@ -23,11 +26,13 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// Whether to allow skipping this operation to resolve a load conflict. /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - public AssetLoadOperation(IModMetadata mod, IModMetadata onBehalfOf, Func getData) + public AssetLoadOperation(IModMetadata mod, bool allowSkipOnConflict, IModMetadata onBehalfOf, Func getData) { this.Mod = mod; + this.AllowSkipOnConflict = allowSkipOnConflict; this.OnBehalfOf = onBehalfOf; this.GetData = getData; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 3b304f0d..43cebcbe 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -608,6 +608,7 @@ namespace StardewModdingAPI.Framework { new AssetLoadOperation( mod: loader.Mod, + allowSkipOnConflict: false, onBehalfOf: null, getData: assetInfo => loader.Data.Load(assetInfo) ) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 1d9c8b4c..b3e98648 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Framework.ContentManagers IAssetInfo info = new AssetInfo(locale, assetName, typeof(object), this.AssertAndNormalizeAssetName); AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); - if (!this.AssertMaxOneLoader(info, loaders, out string error)) + if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error)) this.Monitor.Log(error, LogLevel.Warn); return loaders.Length == 1; @@ -277,13 +277,15 @@ namespace StardewModdingAPI.Framework.ContentManagers { AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); - if (!this.AssertMaxOneLoader(info, loaders, out string error)) + if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error)) { this.Monitor.Log(error, LogLevel.Warn); return null; } - loader = loaders.FirstOrDefault(); + loader = + loaders.FirstOrDefault(p => !p.AllowSkipOnConflict) + ?? loaders.FirstOrDefault(); } // no loader found @@ -392,20 +394,21 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset loaders to apply. /// The error message to show to the user, if the method returns false. /// Returns true if only one loader will apply, else false. - private bool AssertMaxOneLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error) + private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error) { - if (loaders.Length <= 1) + AssetLoadOperation[] required = loaders.Where(p => !p.AllowSkipOnConflict).ToArray(); + if (required.Length <= 1) { error = null; return true; } - string[] loaderNames = loaders + string[] loaderNames = required .Select(p => p.Mod.DisplayName + this.GetOnBehalfOfLabel(p.OnBehalfOf)) .Distinct() .ToArray(); string errorPhrase = loaderNames.Length > 1 - ? $"Multiple mods want to provide '{info.Name}' asset: {string.Join(", ", loaderNames)}" + ? $"Multiple mods want to provide the '{info.Name}' asset: {string.Join(", ", loaderNames)}" : $"The '{loaderNames[0]}' mod wants to provide the '{info.Name}' asset multiple times"; error = $"{errorPhrase}. An asset can't be loaded multiple times, so 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.)"; -- cgit