From e1fc566e0afeb6eb92418bb039365611abd33829 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 Mar 2022 21:46:37 -0400 Subject: add content pack labels (#766) --- src/SMAPI/Events/AssetRequestedEventArgs.cs | 30 ++++++++++++++++---- src/SMAPI/Framework/Content/AssetEditOperation.cs | 7 ++++- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 9 ++++-- src/SMAPI/Framework/ContentCoordinator.cs | 12 ++++++-- .../ContentManagers/GameContentManager.cs | 26 ++++++++++++------ src/SMAPI/Framework/InternalExtensions.cs | 9 ++++++ src/SMAPI/Framework/SCore.cs | 32 +++++++++++++++++++++- 7 files changed, 105 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index b17250b0..774ab808 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI.Events /// The mod handling the event. private readonly IModMetadata Mod; + /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. + private readonly Func GetOnBehalfOf; + /********* ** Accessors @@ -36,14 +39,17 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The mod handling the event. /// The name of the asset being requested. - internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name) + /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. + internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, Func getOnBehalfOf) { this.Mod = mod; this.Name = name; + this.GetOnBehalfOf = getOnBehalfOf; } /// 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. /// /// Usage notes: /// @@ -51,10 +57,14 @@ 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 LoadFrom(Func load) + public void LoadFrom(Func load, string onBehalfOf = null) { this.LoadOperations.Add( - new AssetLoadOperation(this.Mod, _ => load()) + new AssetLoadOperation( + mod: this.Mod, + onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), + getData: _ => load() + ) ); } @@ -71,12 +81,16 @@ namespace StardewModdingAPI.Events public void LoadFromModFile(string relativePath) { this.LoadOperations.Add( - new AssetLoadOperation(this.Mod, _ => this.Mod.Mod.Helper.Content.Load(relativePath)) + new AssetLoadOperation( + mod: this.Mod, + onBehalfOf: null, + _ => this.Mod.Mod.Helper.Content.Load(relativePath)) ); } /// Edit the asset after it's loaded. /// Apply changes to the asset. + /// The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod. /// /// Usage notes: /// @@ -84,10 +98,14 @@ namespace StardewModdingAPI.Events /// You can apply any number of edits to the asset. Each edit will be applied on top of the previous one (i.e. it'll see the merged asset from all previous edits as its input). /// /// - public void Edit(Action apply) + public void Edit(Action apply, string onBehalfOf = null) { this.EditOperations.Add( - new AssetEditOperation(this.Mod, apply) + new AssetEditOperation( + mod: this.Mod, + onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"), + apply + ) ); } } diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs index fa189d44..14db231c 100644 --- a/src/SMAPI/Framework/Content/AssetEditOperation.cs +++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs @@ -11,6 +11,9 @@ namespace StardewModdingAPI.Framework.Content /// The mod applying the edit. public IModMetadata Mod { get; } + /// The content pack on whose behalf the edit is being applied, if any. + public IModMetadata OnBehalfOf { get; } + /// Apply the edit to an asset. public Action ApplyEdit { get; } @@ -20,10 +23,12 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// The content pack on whose behalf the edit is being applied, if any. /// Apply the edit to an asset. - public AssetEditOperation(IModMetadata mod, Action applyEdit) + public AssetEditOperation(IModMetadata mod, IModMetadata onBehalfOf, Action applyEdit) { this.Mod = mod; + this.OnBehalfOf = onBehalfOf; this.ApplyEdit = applyEdit; } } diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index d773cadd..29bf1518 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -8,9 +8,12 @@ namespace StardewModdingAPI.Framework.Content /********* ** Accessors *********/ - /// The mod applying the edit. + /// The mod loading the asset. public IModMetadata Mod { get; } + /// The content pack on whose behalf the asset is being loaded, if any. + public IModMetadata OnBehalfOf { get; } + /// Load the initial value for an asset. public Func GetData { get; } @@ -20,10 +23,12 @@ namespace StardewModdingAPI.Framework.Content *********/ /// Construct an instance. /// The mod applying the edit. + /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - public AssetLoadOperation(IModMetadata mod, Func getData) + public AssetLoadOperation(IModMetadata mod, IModMetadata onBehalfOf, Func getData) { this.Mod = mod; + this.OnBehalfOf = onBehalfOf; this.GetData = getData; } } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 4dbbae15..3b304f0d 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -606,7 +606,11 @@ namespace StardewModdingAPI.Framework mod: loader.Mod, loadOperations: new[] { - new AssetLoadOperation(loader.Mod, assetInfo => loader.Data.Load(assetInfo)) + new AssetLoadOperation( + mod: loader.Mod, + onBehalfOf: null, + getData: assetInfo => loader.Data.Load(assetInfo) + ) }, editOperations: Array.Empty() ); @@ -633,7 +637,11 @@ namespace StardewModdingAPI.Framework loadOperations: Array.Empty(), editOperations: new[] { - new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit(assetData)) + new AssetEditOperation( + mod: editor.Mod, + onBehalfOf: null, + applyEdit: assetData => editor.Data.Edit(assetData) + ) } ); } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 12ed5506..58e36128 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -267,7 +267,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } } - /// Load the initial asset from the registered . + /// Load the initial asset from the registered loaders. /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. private IAssetData ApplyLoader(IAssetInfo info) @@ -296,11 +296,11 @@ namespace StardewModdingAPI.Framework.ContentManagers try { data = (T)loader.GetData(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'."); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.Name}'{this.GetOnBehalfOfLabel(loader.OnBehalfOf)}."); } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when loading asset '{info.Name}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when loading asset '{info.Name}'{this.GetOnBehalfOfLabel(loader.OnBehalfOf)}. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return null; } @@ -310,7 +310,7 @@ namespace StardewModdingAPI.Framework.ContentManagers : null; } - /// Apply any to a loaded asset. + /// Apply any editors to a loaded asset. /// The asset type. /// The basic asset metadata. /// The loaded asset. @@ -343,22 +343,22 @@ namespace StardewModdingAPI.Framework.ContentManagers try { editor.ApplyEdit(asset); - this.Monitor.Log($"{mod.DisplayName} edited {info.Name}."); + this.Monitor.Log($"{mod.DisplayName} edited {info.Name}{this.GetOnBehalfOfLabel(editor.OnBehalfOf)}."); } catch (Exception ex) { - mod.LogAsMod($"Mod crashed when editing asset '{info.Name}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when editing asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)}, which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit if (asset.Data == null) { - mod.LogAsMod($"Mod incorrectly set asset '{info.Name}' to a null value; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to a null value; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } else if (!(asset.Data is T)) { - mod.LogAsMod($"Mod incorrectly set asset '{asset.Name}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{asset.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } } @@ -409,6 +409,16 @@ namespace StardewModdingAPI.Framework.ContentManagers return false; } + /// Get a parenthetical label for log messages for the content pack on whose behalf the action is being performed, if any. + /// The content pack on whose behalf the action is being performed. + private string GetOnBehalfOfLabel(IModMetadata onBehalfOf) + { + if (onBehalfOf == null) + return string.Empty; + + return $" (for the '{onBehalfOf.Manifest.Name}' content pack)"; + } + /// Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible. /// The asset type. /// The basic asset metadata. diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 4cb77a45..fe10b045 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -45,6 +45,15 @@ namespace StardewModdingAPI.Framework metadata.Monitor.Log(message, level); } + /// Log a message using the mod's monitor, but only if it hasn't already been logged since the last game launch. + /// The mod whose monitor to use. + /// The message to log. + /// The log severity level. + public static void LogAsModOnce(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) + { + metadata.Monitor.LogOnce(message, level); + } + /**** ** ManagedEvent ****/ diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9d97ec7d..dd682e40 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1133,7 +1133,7 @@ namespace StardewModdingAPI.Framework this.EventManager.AssetRequested.Raise( invoke: (mod, invoke) => { - AssetRequestedEventArgs args = new(mod, asset.Name); + AssetRequestedEventArgs args = new(mod, asset.Name, this.GetOnBehalfOfContentPack); invoke(args); @@ -1149,6 +1149,36 @@ namespace StardewModdingAPI.Framework return operations; } + /// Get the mod metadata for a content pack whose ID matches , if it's a valid content pack for the given . + /// The mod requesting to act on the content pack's behalf. + /// The content pack ID. + /// The verb phrase indicating what action will be performed, like 'load assets' or 'edit assets'. + /// Returns the content pack metadata if valid, else null. + private IModMetadata GetOnBehalfOfContentPack(IModMetadata mod, string id, string verb) + { + if (id == null) + return null; + + string errorPrefix = $"Can't {verb} on behalf of content pack ID '{id}'"; + + // get target mod + IModMetadata onBehalfOf = this.ModRegistry.Get(id); + if (onBehalfOf == null) + { + mod.LogAsModOnce($"{errorPrefix}: there's no content pack installed with that ID.", LogLevel.Warn); + return null; + } + + // make sure it's a content pack for the requesting mod + if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest?.ContentPackFor?.UniqueID, mod.Manifest.UniqueID)) + { + mod.LogAsModOnce($"{errorPrefix}: that isn't a content pack for this mod.", LogLevel.Warn); + return null; + } + + return onBehalfOf; + } + /// Raised immediately before the player returns to the title screen. private void OnReturningToTitle() { -- cgit