summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets2
-rw-r--r--docs/release-notes.md15
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json9
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Events/AssetRequestedEventArgs.cs43
-rw-r--r--src/SMAPI/Framework/Content/AssetOperationGroup.cs7
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs186
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs71
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs27
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs34
-rw-r--r--src/SMAPI/Framework/Events/IManagedEvent.cs4
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs101
-rw-r--r--src/SMAPI/Framework/Exceptions/ContentLoadErrorType.cs21
-rw-r--r--src/SMAPI/Framework/Exceptions/SContentLoadException.cs15
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs3
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs31
-rw-r--r--src/SMAPI/Framework/ModHelpers/GameContentHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModContentHelper.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs208
-rw-r--r--src/SMAPI/Framework/SGame.cs4
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs12
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs4
26 files changed, 483 insertions, 336 deletions
diff --git a/build/common.targets b/build/common.targets
index 0436ed5b..a7db917c 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
- <Version>3.14.2</Version>
+ <Version>3.14.3</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 98747613..6311d7dc 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,6 +1,21 @@
← [README](README.md)
# Release notes
+## 3.14.3
+Released 12 May 2022 for Stardew Valley 1.5.6 or later.
+
+* For players:
+ * Reduced in-game performance impact.
+
+* For mod authors:
+ * Refactored how event handling works under the hood, particularly the new content API. This should have no effect on mod usage.
+ * Verbose mode now logs the in-game time.
+ * Fixed error when loading a `.xnb` file through the old content API without the file extension.
+ * Fixed asset propagation for player sprites not fully updating recolor masks in some cases.
+
+* For the web UI:
+ * Updated the JSON validator/schema for Content Patcher 1.26.0.
+
## 3.14.2
Released 08 May 2022 for Stardew Valley 1.5.6 or later.
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index c263456a..0e2b023d 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.14.2",
+ "Version": "3.14.3",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.14.2"
+ "MinimumApiVersion": "3.14.3"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index 6e6a271f..f449b3bd 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.14.2",
+ "Version": "3.14.3",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.14.2"
+ "MinimumApiVersion": "3.14.3"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 5ba91568..23e241b5 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.14.2",
+ "Version": "3.14.3",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.14.2"
+ "MinimumApiVersion": "3.14.3"
}
diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
index 4975a973..f0fe74c2 100644
--- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
+++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
@@ -14,9 +14,9 @@
"title": "Format version",
"description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.",
"type": "string",
- "const": "1.25.0",
+ "const": "1.26.0",
"@errorMessages": {
- "const": "Incorrect value '@value'. You should always use the latest format version (currently 1.25.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
+ "const": "Incorrect value '@value'. You should always use the latest format version (currently 1.26.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
}
},
"ConfigSchema": {
@@ -51,6 +51,11 @@
"description": "An optional explanation of the config field for players, shown in UIs like Generic Mod Config Menu.",
"type": "string"
},
+ "Section": {
+ "title": "Section",
+ "description": "An optional section key to group related fields on config UIs. This can be the literal text to show, or you can add a translation with the key 'config.section.<section value>.name' and '.description' to add a translated name & tooltip.",
+ "type": "string"
+ },
"additionalProperties": false
},
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index a289ce4b..ef729f4f 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -50,7 +50,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.14.2";
+ internal static string RawApiVersion = "3.14.3";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs
index d0aef1db..d6561028 100644
--- a/src/SMAPI/Events/AssetRequestedEventArgs.cs
+++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs
@@ -14,7 +14,7 @@ namespace StardewModdingAPI.Events
** Fields
*********/
/// <summary>The mod handling the event.</summary>
- private readonly IModMetadata Mod;
+ private 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;
@@ -37,26 +37,31 @@ namespace StardewModdingAPI.Events
public Type DataType => this.AssetInfo.DataType;
/// <summary>The load operations requested by the event handler.</summary>
- internal IList<AssetLoadOperation> LoadOperations { get; } = new List<AssetLoadOperation>();
+ internal List<AssetLoadOperation> LoadOperations { get; } = new();
/// <summary>The edit operations requested by the event handler.</summary>
- internal IList<AssetEditOperation> EditOperations { get; } = new List<AssetEditOperation>();
+ internal List<AssetEditOperation> EditOperations { get; } = new();
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="mod">The mod handling the event.</param>
/// <param name="assetInfo">The asset info being requested.</param>
/// <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, IAssetInfo assetInfo, Func<IModMetadata, string?, string, IModMetadata?> getOnBehalfOf)
+ internal AssetRequestedEventArgs(IAssetInfo assetInfo, Func<IModMetadata, string?, string, IModMetadata?> getOnBehalfOf)
{
- this.Mod = mod;
this.AssetInfo = assetInfo;
this.GetOnBehalfOf = getOnBehalfOf;
}
+ /// <summary>Set the mod handling the event.</summary>
+ /// <param name="mod">The mod handling the event.</param>
+ internal void SetMod(IModMetadata mod)
+ {
+ this.Mod = mod;
+ }
+
/// <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="priority">If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</param>
@@ -70,10 +75,11 @@ namespace StardewModdingAPI.Events
/// </remarks>
public void LoadFrom(Func<object> load, AssetLoadPriority priority, string? onBehalfOf = null)
{
+ IModMetadata mod = this.GetMod();
this.LoadOperations.Add(
new AssetLoadOperation(
- Mod: this.Mod,
- OnBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"),
+ Mod: mod,
+ OnBehalfOf: this.GetOnBehalfOf(mod, onBehalfOf, "load assets"),
Priority: priority,
GetData: _ => load()
)
@@ -94,12 +100,13 @@ namespace StardewModdingAPI.Events
public void LoadFromModFile<TAsset>(string relativePath, AssetLoadPriority priority)
where TAsset : notnull
{
+ IModMetadata mod = this.GetMod();
this.LoadOperations.Add(
new AssetLoadOperation(
- Mod: this.Mod,
+ Mod: mod,
OnBehalfOf: null,
Priority: priority,
- GetData: _ => this.Mod.Mod!.Helper.ModContent.Load<TAsset>(relativePath)
+ GetData: _ => mod.Mod!.Helper.ModContent.Load<TAsset>(relativePath)
)
);
}
@@ -117,14 +124,26 @@ namespace StardewModdingAPI.Events
/// </remarks>
public void Edit(Action<IAssetData> apply, AssetEditPriority priority = AssetEditPriority.Default, string? onBehalfOf = null)
{
+ IModMetadata mod = this.GetMod();
this.EditOperations.Add(
new AssetEditOperation(
- Mod: this.Mod,
+ Mod: mod,
Priority: priority,
- OnBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"),
+ OnBehalfOf: this.GetOnBehalfOf(mod, onBehalfOf, "edit assets"),
ApplyEdit: apply
)
);
}
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the mod handling the event.</summary>
+ /// <exception cref="InvalidOperationException">This instance hasn't been initialized with the mod metadata yet.</exception>
+ private IModMetadata GetMod()
+ {
+ return this.Mod ?? throw new InvalidOperationException($"This {nameof(AssetRequestedEventArgs)} instance hasn't been initialized yet.");
+ }
}
}
diff --git a/src/SMAPI/Framework/Content/AssetOperationGroup.cs b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
index 1566a8f0..11767d39 100644
--- a/src/SMAPI/Framework/Content/AssetOperationGroup.cs
+++ b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
@@ -1,8 +1,9 @@
+using System.Collections.Generic;
+
namespace StardewModdingAPI.Framework.Content
{
- /// <summary>A set of operations to apply to an asset for a given <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/> implementation.</summary>
- /// <param name="Mod">The mod applying the changes.</param>
+ /// <summary>A set of operations to apply to an asset.</summary>
/// <param name="LoadOperations">The load operations to apply.</param>
/// <param name="EditOperations">The edit operations to apply.</param>
- internal record AssetOperationGroup(IModMetadata Mod, AssetLoadOperation[] LoadOperations, AssetEditOperation[] EditOperations);
+ internal record AssetOperationGroup(List<AssetLoadOperation> LoadOperations, List<AssetEditOperation> EditOperations);
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 6702f5e6..a24581a0 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework
private readonly Action<IList<IAssetName>> OnAssetsInvalidated;
/// <summary>Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</summary>
- private readonly Func<IAssetInfo, IList<AssetOperationGroup>> RequestAssetOperations;
+ private readonly Func<IAssetInfo, AssetOperationGroup?> RequestAssetOperations;
/// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
private readonly List<IContentManager> ContentManagers = new();
@@ -79,15 +79,15 @@ namespace StardewModdingAPI.Framework
private Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
/// <summary>The cached asset load/edit operations to apply, indexed by asset name.</summary>
- private readonly TickCacheDictionary<IAssetName, IList<AssetOperationGroup>> AssetOperationsByKey = new();
+ private readonly TickCacheDictionary<IAssetName, AssetOperationGroup?> AssetOperationsByKey = new();
/// <summary>A cache of asset operation groups created for legacy <see cref="IAssetLoader"/> implementations.</summary>
[Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")]
- private readonly Dictionary<IAssetLoader, Dictionary<Type, AssetOperationGroup>> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance);
+ private readonly Dictionary<IAssetLoader, Dictionary<Type, AssetLoadOperation>> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance);
/// <summary>A cache of asset operation groups created for legacy <see cref="IAssetEditor"/> implementations.</summary>
[Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")]
- private readonly Dictionary<IAssetEditor, Dictionary<Type, AssetOperationGroup>> LegacyEditorCache = new(ReferenceEqualityComparer.Instance);
+ private readonly Dictionary<IAssetEditor, Dictionary<Type, AssetEditOperation>> LegacyEditorCache = new(ReferenceEqualityComparer.Instance);
/*********
@@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, IList<AssetOperationGroup>> requestAssetOperations)
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
{
this.GetFileLookup = getFileLookup;
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
@@ -449,16 +449,12 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the asset load and edit operations to apply to a given asset if it's (re)loaded now.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The asset info to load or edit.</param>
- public IList<AssetOperationGroup> GetAssetOperations<T>(IAssetInfo info)
+ public AssetOperationGroup? GetAssetOperations<T>(IAssetInfo info)
where T : notnull
{
return this.AssetOperationsByKey.GetOrSet(
info.Name,
-#pragma warning disable CS0612, CS0618 // deprecated code
- () => this.Editors.Count > 0 || this.Loaders.Count > 0
- ? this.GetAssetOperationsIncludingLegacyWithoutCache<T>(info).ToArray()
-#pragma warning restore CS0612, CS0618
- : this.RequestAssetOperations(info)
+ () => this.GetAssetOperationsWithoutCache<T>(info)
);
}
@@ -584,41 +580,40 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the asset load and edit operations to apply to a given asset if it's (re)loaded now, ignoring the <see cref="AssetOperationsByKey"/> cache.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The asset info to load or edit.</param>
- [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")]
- private IEnumerable<AssetOperationGroup> GetAssetOperationsIncludingLegacyWithoutCache<T>(IAssetInfo info)
+ private AssetOperationGroup? GetAssetOperationsWithoutCache<T>(IAssetInfo info)
where T : notnull
{
- IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info);
-
// new content API
- foreach (AssetOperationGroup group in this.RequestAssetOperations(info))
- yield return group;
+ AssetOperationGroup? group = this.RequestAssetOperations(info);
// legacy load operations
- foreach (ModLinked<IAssetLoader> loader in this.Loaders)
+#pragma warning disable CS0612, CS0618 // deprecated code
+ if (this.Editors.Count > 0 || this.Loaders.Count > 0)
{
- // check if loader applies
- try
+ IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info);
+
+ foreach (ModLinked<IAssetLoader> loader in this.Loaders)
{
- if (!loader.Data.CanLoad<T>(legacyInfo))
+ // check if loader applies
+ try
+ {
+ if (!loader.Data.CanLoad<T>(legacyInfo))
+ continue;
+ }
+ catch (Exception ex)
+ {
+ loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
- }
- catch (Exception ex)
- {
- loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
+ }
- // add operation
- yield return this.GetOrCreateLegacyOperationGroup(
- cache: this.LegacyLoaderCache,
- editor: loader.Data,
- dataType: info.DataType,
- createGroup: () => new AssetOperationGroup(
- Mod: loader.Mod,
- LoadOperations: new[]
- {
- new AssetLoadOperation(
+ // add operation
+ group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>());
+ group.LoadOperations.Add(
+ this.GetOrCreateLegacyOperation(
+ cache: this.LegacyLoaderCache,
+ editor: loader.Data,
+ dataType: info.DataType,
+ create: () => new AssetLoadOperation(
Mod: loader.Mod,
OnBehalfOf: null,
Priority: AssetLoadPriority.Exclusive,
@@ -626,59 +621,54 @@ namespace StardewModdingAPI.Framework
this.GetLegacyAssetInfo(assetInfo)
)
)
- },
- EditOperations: Array.Empty<AssetEditOperation>()
- )
- );
- }
+ )
+ );
+ }
- // legacy edit operations
- foreach (var editor in this.Editors)
- {
- // check if editor applies
- try
+ // legacy edit operations
+ foreach (var editor in this.Editors)
{
- if (!editor.Data.CanEdit<T>(legacyInfo))
+ // check if editor applies
+ try
+ {
+ if (!editor.Data.CanEdit<T>(legacyInfo))
+ continue;
+ }
+ catch (Exception ex)
+ {
+ editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
- }
- catch (Exception ex)
- {
- editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
+ }
- // HACK
- //
- // If two editors have the same priority, they're applied in registration order (so
- // whichever was registered first is applied first). Mods often depend on this
- // behavior, like Json Assets registering its interceptors before Content Patcher.
- //
- // Unfortunately the old & new content APIs have separate lists, so new-API
- // interceptors always ran before old-API interceptors with the same priority,
- // regardless of the registration order *between* APIs. Since the new API works in
- // a fundamentally different way (i.e. loads/edits are defined on asset request
- // instead of by registering a global 'editor' or 'loader' class), there's no way
- // to track registration order between them.
- //
- // Until we drop the old content API in SMAPI 4.0.0, this sets the priority for
- // specific legacy editors to maintain compatibility.
- AssetEditPriority priority = editor.Data.GetType().FullName switch
- {
- "JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher
- _ => AssetEditPriority.Default
- };
-
- // add operation
- yield return this.GetOrCreateLegacyOperationGroup(
- cache: this.LegacyEditorCache,
- editor: editor.Data,
- dataType: info.DataType,
- createGroup: () => new AssetOperationGroup(
- Mod: editor.Mod,
- LoadOperations: Array.Empty<AssetLoadOperation>(),
- EditOperations: new[]
- {
- new AssetEditOperation(
+ // HACK
+ //
+ // If two editors have the same priority, they're applied in registration order (so
+ // whichever was registered first is applied first). Mods often depend on this
+ // behavior, like Json Assets registering its interceptors before Content Patcher.
+ //
+ // Unfortunately the old & new content APIs have separate lists, so new-API
+ // interceptors always ran before old-API interceptors with the same priority,
+ // regardless of the registration order *between* APIs. Since the new API works in
+ // a fundamentally different way (i.e. loads/edits are defined on asset request
+ // instead of by registering a global 'editor' or 'loader' class), there's no way
+ // to track registration order between them.
+ //
+ // Until we drop the old content API in SMAPI 4.0.0, this sets the priority for
+ // specific legacy editors to maintain compatibility.
+ AssetEditPriority priority = editor.Data.GetType().FullName switch
+ {
+ "JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher
+ _ => AssetEditPriority.Default
+ };
+
+ // add operation
+ group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>());
+ group.EditOperations.Add(
+ this.GetOrCreateLegacyOperation(
+ cache: this.LegacyEditorCache,
+ editor: editor.Data,
+ dataType: info.DataType,
+ create: () => new AssetEditOperation(
Mod: editor.Mod,
OnBehalfOf: null,
Priority: priority,
@@ -686,28 +676,32 @@ namespace StardewModdingAPI.Framework
this.GetLegacyAssetData(assetData)
)
)
- }
- )
- );
+ )
+ );
+ }
}
+#pragma warning restore CS0612, CS0618
+
+ return group;
}
/// <summary>Get a cached asset operation group for a legacy <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/> instance, creating it if needed.</summary>
/// <typeparam name="TInterceptor">The editor type (one of <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/>).</typeparam>
+ /// <typeparam name="TOperation">The operation model type.</typeparam>
/// <param name="cache">The cached operation groups for the interceptor type.</param>
/// <param name="editor">The legacy asset interceptor.</param>
/// <param name="dataType">The asset data type.</param>
- /// <param name="createGroup">Create the asset operation group if it's not cached yet.</param>
- private AssetOperationGroup GetOrCreateLegacyOperationGroup<TInterceptor>(Dictionary<TInterceptor, Dictionary<Type, AssetOperationGroup>> cache, TInterceptor editor, Type dataType, Func<AssetOperationGroup> createGroup)
+ /// <param name="create">Create the asset operation group if it's not cached yet.</param>
+ private TOperation GetOrCreateLegacyOperation<TInterceptor, TOperation>(Dictionary<TInterceptor, Dictionary<Type, TOperation>> cache, TInterceptor editor, Type dataType, Func<TOperation> create)
where TInterceptor : class
{
- if (!cache.TryGetValue(editor, out Dictionary<Type, AssetOperationGroup>? cacheByType))
- cache[editor] = cacheByType = new Dictionary<Type, AssetOperationGroup>();
+ if (!cache.TryGetValue(editor, out Dictionary<Type, TOperation>? cacheByType))
+ cache[editor] = cacheByType = new Dictionary<Type, TOperation>();
- if (!cacheByType.TryGetValue(dataType, out AssetOperationGroup? group))
- cacheByType[dataType] = group = createGroup();
+ if (!cacheByType.TryGetValue(dataType, out TOperation? operation))
+ cacheByType[dataType] = operation = create();
- return group;
+ return operation;
}
/// <summary>Get an asset info compatible with legacy <see cref="IAssetLoader"/> and <see cref="IAssetEditor"/> instances, which always expect the base name.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index e4695588..575d252e 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
// NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid
// throwing other types like ArgumentException here.
if (string.IsNullOrWhiteSpace(assetName))
- throw new SContentLoadException("The asset key or local path is empty.");
+ throw new SContentLoadException(ContentLoadErrorType.InvalidName, "The asset key or local path is empty.");
if (assetName.Intersect(Path.GetInvalidPathChars()).Any())
- throw new SContentLoadException("The asset key or local path contains invalid characters.");
+ throw new SContentLoadException(ContentLoadErrorType.InvalidName, "The asset key or local path contains invalid characters.");
return this.Cache.NormalizeKey(assetName);
}
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index c53040e1..2aa50542 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -75,15 +75,19 @@ namespace StardewModdingAPI.Framework.ContentManagers
// custom asset from a loader
string locale = this.GetLocale();
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName);
- AssetLoadOperation[] loaders = this.GetLoaders<object>(info).ToArray();
-
- if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error))
+ AssetOperationGroup? operations = this.Coordinator.GetAssetOperations<object>(info);
+ if (operations?.LoadOperations.Count > 0)
{
- this.Monitor.Log(error, LogLevel.Warn);
- return false;
+ if (!this.AssertMaxOneRequiredLoader(info, operations.LoadOperations, out string? error))
+ {
+ this.Monitor.Log(error, LogLevel.Warn);
+ return false;
+ }
+
+ return true;
}
- return loaders.Any();
+ return false;
}
/// <inheritdoc />
@@ -121,10 +125,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
data = this.AssetsBeingLoaded.Track(assetName.Name, () =>
{
IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName);
+