diff options
Diffstat (limited to 'src/SMAPI/Framework')
| -rw-r--r-- | src/SMAPI/Framework/Content/AssetOperationGroup.cs | 7 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 186 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 4 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 71 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 27 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/EventManager.cs | 34 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/IManagedEvent.cs | 4 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/ManagedEvent.cs | 101 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Exceptions/ContentLoadErrorType.cs | 21 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Exceptions/SContentLoadException.cs | 15 | ||||
| -rw-r--r-- | src/SMAPI/Framework/InternalExtensions.cs | 3 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 31 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/GameContentHelper.cs | 2 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModContentHelper.cs | 2 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SCore.cs | 208 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SGame.cs | 4 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SMultiplayer.cs | 12 |
17 files changed, 419 insertions, 313 deletions
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); + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations<T>(info); IAssetData asset = - this.ApplyLoader<T>(info) + this.ApplyLoader<T>(info, operations?.LoadOperations) ?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection); - asset = this.ApplyEditors<T>(info, asset); + asset = this.ApplyEditors<T>(info, asset, operations?.EditOperations); return (T)asset.Data; }); } @@ -149,25 +154,23 @@ namespace StardewModdingAPI.Framework.ContentManagers *********/ /// <summary>Load the initial asset from the registered loaders.</summary> /// <param name="info">The basic asset metadata.</param> + /// <param name="loadOperations">The load operations to apply to the asset.</param> /// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns> - private IAssetData? ApplyLoader<T>(IAssetInfo info) + private IAssetData? ApplyLoader<T>(IAssetInfo info, List<AssetLoadOperation>? loadOperations) where T : notnull { // find matching loader - AssetLoadOperation? loader; + AssetLoadOperation? loader = null; + if (loadOperations?.Count > 0) { - AssetLoadOperation[] loaders = this.GetLoaders<T>(info).OrderByDescending(p => p.Priority).ToArray(); - - if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error)) + if (!this.AssertMaxOneRequiredLoader(info, loadOperations, out string? error)) { this.Monitor.Log(error, LogLevel.Warn); return null; } - loader = loaders.FirstOrDefault(); + loader = loadOperations.OrderByDescending(p => p.Priority).FirstOrDefault(); } - - // no loader found if (loader == null) return null; @@ -195,9 +198,13 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <typeparam name="T">The asset type.</typeparam> /// <param name="info">The basic asset metadata.</param> /// <param name="asset">The loaded asset.</param> - private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset) + /// <param name="editOperations">The edit operations to apply to the asset.</param> + private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset, List<AssetEditOperation>? editOperations) where T : notnull { + if (editOperations?.Count is not > 0) + return asset; + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName, this.Reflection); // special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead. @@ -210,12 +217,12 @@ namespace StardewModdingAPI.Framework.ContentManagers return (IAssetData)this.GetType() .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)! .MakeGenericMethod(actualType) - .Invoke(this, new object[] { info, asset })!; + .Invoke(this, new object[] { info, asset, editOperations })!; } } // edit asset - AssetEditOperation[] editors = this.GetEditors<T>(info).OrderBy(p => p.Priority).ToArray(); + AssetEditOperation[] editors = editOperations.OrderBy(p => p.Priority).ToArray(); foreach (AssetEditOperation editor in editors) { IModMetadata mod = editor.Mod; @@ -250,34 +257,12 @@ namespace StardewModdingAPI.Framework.ContentManagers return asset; } - /// <summary>Get the asset loaders which handle an asset.</summary> - /// <typeparam name="T">The asset type.</typeparam> - /// <param name="info">The basic asset metadata.</param> - private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info) - where T : notnull - { - return this.Coordinator - .GetAssetOperations<T>(info) - .SelectMany(p => p.LoadOperations); - } - - /// <summary>Get the asset editors to apply to an asset.</summary> - /// <typeparam name="T">The asset type.</typeparam> - /// <param name="info">The basic asset metadata.</param> - private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info) - where T : notnull - { - return this.Coordinator - .GetAssetOperations<T>(info) - .SelectMany(p => p.EditOperations); - } - /// <summary>Assert that at most one loader will be applied to an asset.</summary> /// <param name="info">The basic asset metadata.</param> /// <param name="loaders">The asset loaders to apply.</param> /// <param name="error">The error message to show to the user, if the method returns false.</param> /// <returns>Returns true if only one loader will apply, else false.</returns> - private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, [NotNullWhen(false)] out string? error) + private bool AssertMaxOneRequiredLoader(IAssetInfo info, List<AssetLoadOperation> loaders, [NotNullWhen(false)] out string? error) { AssetLoadOperation[] required = loaders.Where(p => p.Priority == AssetLoadPriority.Exclusive).ToArray(); if (required.Length <= 1) @@ -295,7 +280,7 @@ namespace StardewModdingAPI.Framework.ContentManagers ? $"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.)"; + 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 avoid {nameof(AssetLoadPriority)}.{nameof(AssetLoadPriority.Exclusive)} and {nameof(IAssetLoader)} if possible to avoid conflicts.)"; return false; } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 7cac8f36..85e109c8 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath)) { if (contentManagerID != this.Name) - throw this.GetLoadError(assetName, "can't load a different mod's managed asset key through this mod content manager."); + throw this.GetLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager."); assetName = relativePath; } } @@ -106,7 +106,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get file FileInfo file = this.GetModFile<T>(assetName.Name); if (!file.Exists) - throw this.GetLoadError(assetName, "the specified path doesn't exist."); + throw this.GetLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist."); // load content asset = file.Extension.ToLower() switch @@ -121,7 +121,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) when (ex is not SContentLoadException) { - throw this.GetLoadError(assetName, "an unexpected occurred.", ex); + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected occurred.", ex); } // track & return asset @@ -157,7 +157,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (!typeof(T).IsAssignableFrom(typeof(XmlSource))) - throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'."); // load string source = File.ReadAllText(file.FullName); @@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadDataFile<T>(IAssetName assetName, FileInfo file) { if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset)) - throw this.GetLoadError(assetName, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method return asset; } @@ -184,7 +184,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (typeof(T) != typeof(Texture2D)) - throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); // load using FileStream stream = File.OpenRead(file.FullName); @@ -201,7 +201,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (typeof(T) != typeof(Map)) - throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); // load FormatManager formatManager = FormatManager.Instance; @@ -239,16 +239,17 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="file">The file to load.</param> private T HandleUnknownFileType<T>(IAssetName assetName, FileInfo file) { - throw this.GetLoadError(assetName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); } /// <summary>Get an error which indicates that an asset couldn't be loaded.</summary> + /// <param name="errorType">Why loading an asset through the content pipeline failed.</param> /// <param name="assetName">The asset name that failed to load.</param> /// <param name="reasonPhrase">The reason the file couldn't be loaded.</param> /// <param name="exception">The underlying exception, if applicable.</param> - private SContentLoadException GetLoadError(IAssetName assetName, string reasonPhrase, Exception? exception = null) + private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { - return new($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); + return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); } /// <summary>Get a file from the mod folder.</summary> @@ -328,13 +329,13 @@ namespace StardewModdingAPI.Framework.ContentManagers // validate tilesheet path string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) - throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../)."); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../)."); // load best match try { if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName? assetName, out string? error)) - throw new SContentLoadException($"{errorPrefix} {error}"); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} {error}"); if (assetName is not null) { @@ -346,7 +347,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) when (ex is not SContentLoadException) { - throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex); } } } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 41540047..b21d5c7d 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.Events public EventManager(ModRegistry modRegistry) { // create shortcut initializers - ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName, bool isPerformanceCritical = false) + ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) { - return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry, isPerformanceCritical); + return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry); } // init events @@ -210,21 +210,21 @@ namespace StardewModdingAPI.Framework.Events this.LocaleChanged = ManageEventOf<LocaleChangedEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged)); this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); - this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); - this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true); - this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true); - this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IM |
