summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md2
-rw-r--r--src/SMAPI/Events/AssetRequestedEventArgs.cs30
-rw-r--r--src/SMAPI/Framework/Content/AssetEditOperation.cs7
-rw-r--r--src/SMAPI/Framework/Content/AssetLoadOperation.cs9
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs12
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs26
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs9
-rw-r--r--src/SMAPI/Framework/SCore.cs32
8 files changed, 106 insertions, 21 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 8bf451a5..2a8d142f 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -11,7 +11,7 @@
* Fixed `--no-terminal` still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!).
* For mod authors:
- * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.
+ * Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0. These include new features not supported by the old API like content pack labels.
* Overhauled [mod-provided APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) (thanks to Shockah!).
_This adds support for many previously-unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more._
* Added `Constants.ContentPath`.
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
/// <summary>The mod handling the event.</summary>
private readonly IModMetadata Mod;
+ /// <summary>Get the mod metadata for a content pack, if it's a valid content pack for the mod.</summary>
+ private readonly Func<IModMetadata, string, string, IModMetadata> GetOnBehalfOf;
+
/*********
** Accessors
@@ -36,14 +39,17 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod handling the event.</param>
/// <param name="name">The name of the asset being requested.</param>
- internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name)
+ /// <param name="getOnBehalfOf">Get the mod metadata for a content pack, if it's a valid content pack for the mod.</param>
+ internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, Func<IModMetadata, string, string, IModMetadata> getOnBehalfOf)
{
this.Mod = mod;
this.Name = name;
+ this.GetOnBehalfOf = getOnBehalfOf;
}
/// <summary>Provide the initial instance for the asset, instead of trying to load it from the game's <c>Content</c> folder.</summary>
/// <param name="load">Get the initial instance of an asset.</param>
+ /// <param name="onBehalfOf">The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod.</param>
/// <remarks>
/// Usage notes:
/// <list type="bullet">
@@ -51,10 +57,14 @@ namespace StardewModdingAPI.Events
/// <item>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 <see cref="Edit"/> instead to avoid those limitations and improve mod compatibility.</item>
/// </list>
/// </remarks>
- public void LoadFrom(Func<object> load)
+ public void LoadFrom(Func<object> 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<TAsset>(string relativePath)
{
this.LoadOperations.Add(
- new AssetLoadOperation(this.Mod, _ => this.Mod.Mod.Helper.Content.Load<TAsset>(relativePath))
+ new AssetLoadOperation(
+ mod: this.Mod,
+ onBehalfOf: null,
+ _ => this.Mod.Mod.Helper.Content.Load<TAsset>(relativePath))
);
}
/// <summary>Edit the asset after it's loaded.</summary>
/// <param name="apply">Apply changes to the asset.</param>
+ /// <param name="onBehalfOf">The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod.</param>
/// <remarks>
/// Usage notes:
/// <list type="bullet">
@@ -84,10 +98,14 @@ namespace StardewModdingAPI.Events
/// <item>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).</item>
/// </list>
/// </remarks>
- public void Edit(Action<IAssetData> apply)
+ public void Edit(Action<IAssetData> 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
/// <summary>The mod applying the edit.</summary>
public IModMetadata Mod { get; }
+ /// <summary>The content pack on whose behalf the edit is being applied, if any.</summary>
+ public IModMetadata OnBehalfOf { get; }
+
/// <summary>Apply the edit to an asset.</summary>
public Action<IAssetData> ApplyEdit { get; }
@@ -20,10 +23,12 @@ namespace StardewModdingAPI.Framework.Content
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod applying the edit.</param>
+ /// <param name="onBehalfOf">The content pack on whose behalf the edit is being applied, if any.</param>
/// <param name="applyEdit">Apply the edit to an asset.</param>
- public AssetEditOperation(IModMetadata mod, Action<IAssetData> applyEdit)
+ public AssetEditOperation(IModMetadata mod, IModMetadata onBehalfOf, Action<IAssetData> 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
*********/
- /// <summary>The mod applying the edit.</summary>
+ /// <summary>The mod loading the asset.</summary>
public IModMetadata Mod { get; }
+ /// <summary>The content pack on whose behalf the asset is being loaded, if any.</summary>
+ public IModMetadata OnBehalfOf { get; }
+
/// <summary>Load the initial value for an asset.</summary>
public Func<IAssetInfo, object> GetData { get; }
@@ -20,10 +23,12 @@ namespace StardewModdingAPI.Framework.Content
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod applying the edit.</param>
+ /// <param name="onBehalfOf">The content pack on whose behalf the asset is being loaded, if any.</param>
/// <param name="getData">Load the initial value for an asset.</param>
- public AssetLoadOperation(IModMetadata mod, Func<IAssetInfo, object> getData)
+ public AssetLoadOperation(IModMetadata mod, IModMetadata onBehalfOf, Func<IAssetInfo, object> 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<T>(assetInfo))
+ new AssetLoadOperation(
+ mod: loader.Mod,
+ onBehalfOf: null,
+ getData: assetInfo => loader.Data.Load<T>(assetInfo)
+ )
},
editOperations: Array.Empty<AssetEditOperation>()
);
@@ -633,7 +637,11 @@ namespace StardewModdingAPI.Framework
loadOperations: Array.Empty<AssetLoadOperation>(),
editOperations: new[]
{
- new AssetEditOperation(editor.Mod, assetData => editor.Data.Edit<T>(assetData))
+ new AssetEditOperation(
+ mod: editor.Mod,
+ onBehalfOf: null,
+ applyEdit: assetData => editor.Data.Edit<T>(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
}
}
- /// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
+ /// <summary>Load the initial asset from the registered loaders.</summary>
/// <param name="info">The basic asset metadata.</param>
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
private IAssetData ApplyLoader<T>(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;
}
- /// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary>
+ /// <summary>Apply any editors to a loaded asset.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
/// <param name="asset">The loaded asset.</param>
@@ -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;
}
+ /// <summary>Get a parenthetical label for log messages for the content pack on whose behalf the action is being performed, if any.</summary>
+ /// <param name="onBehalfOf">The content pack on whose behalf the action is being performed.</param>
+ private string GetOnBehalfOfLabel(IModMetadata onBehalfOf)
+ {
+ if (onBehalfOf == null)
+ return string.Empty;
+
+ return $" (for the '{onBehalfOf.Manifest.Name}' content pack)";
+ }
+
/// <summary>Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
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);
}
+ /// <summary>Log a message using the mod's monitor, but only if it hasn't already been logged since the last game launch.</summary>
+ /// <param name="metadata">The mod whose monitor to use.</param>
+ /// <param name="message">The message to log.</param>
+ /// <param name="level">The log severity level.</param>
+ 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;
}
+ /// <summary>Get the mod metadata for a content pack whose ID matches <paramref name="id"/>, if it's a valid content pack for the given <paramref name="mod"/>.</summary>
+ /// <param name="mod">The mod requesting to act on the content pack's behalf.</param>
+ /// <param name="id">The content pack ID.</param>
+ /// <param name="verb">The verb phrase indicating what action will be performed, like 'load assets' or 'edit assets'.</param>
+ /// <returns>Returns the content pack metadata if valid, else <c>null</c>.</returns>
+ 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;
+ }
+
/// <summary>Raised immediately before the player returns to the title screen.</summary>
private void OnReturningToTitle()
{