diff options
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); + |
