From e14916f9622592a993fad54fe184a03de0b95c7b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 17:12:58 -0400 Subject: add error code to SContentLoadException --- .../ContentManagers/BaseContentManager.cs | 4 ++-- .../Framework/ContentManagers/ModContentManager.cs | 27 +++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') 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/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(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(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 /// The file to load. private T HandleUnknownFileType(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'."); } /// Get an error which indicates that an asset couldn't be loaded. + /// Why loading an asset through the content pipeline failed. /// The asset name that failed to load. /// The reason the file couldn't be loaded. /// The underlying exception, if applicable. - 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); } /// Get a file from the mod folder. @@ -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); } } } -- cgit From bbe5983acdd082d2185a69e2ad37d659a298223d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 21:36:45 -0400 Subject: rewrite asset operations to reduce allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • When raising AssetRequested, SMAPI now creates a single event args model and reuses it for each handler. • There's now a single AssetOperationGroup per asset, which tracks the loaders/editors registered by every mod for that asset. • The operation group's loader/editor lists are now used directly instead of querying them. --- .../ContentManagers/GameContentManager.cs | 71 +++++++++------------- 1 file changed, 28 insertions(+), 43 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') 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(info).ToArray(); - - if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error)) + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(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; } /// @@ -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(info); IAssetData asset = - this.ApplyLoader(info) + this.ApplyLoader(info, operations?.LoadOperations) ?? new AssetDataForObject(info, this.RawLoad(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection); - asset = this.ApplyEditors(info, asset); + asset = this.ApplyEditors(info, asset, operations?.EditOperations); return (T)asset.Data; }); } @@ -149,25 +154,23 @@ namespace StardewModdingAPI.Framework.ContentManagers *********/ /// Load the initial asset from the registered loaders. /// The basic asset metadata. + /// The load operations to apply to the asset. /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData? ApplyLoader(IAssetInfo info) + private IAssetData? ApplyLoader(IAssetInfo info, List? loadOperations) where T : notnull { // find matching loader - AssetLoadOperation? loader; + AssetLoadOperation? loader = null; + if (loadOperations?.Count > 0) { - AssetLoadOperation[] loaders = this.GetLoaders(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 /// The asset type. /// The basic asset metadata. /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + /// The edit operations to apply to the asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset, List? 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(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; } - /// Get the asset loaders which handle an asset. - /// The asset type. - /// The basic asset metadata. - private IEnumerable GetLoaders(IAssetInfo info) - where T : notnull - { - return this.Coordinator - .GetAssetOperations(info) - .SelectMany(p => p.LoadOperations); - } - - /// Get the asset editors to apply to an asset. - /// The asset type. - /// The basic asset metadata. - private IEnumerable GetEditors(IAssetInfo info) - where T : notnull - { - return this.Coordinator - .GetAssetOperations(info) - .SelectMany(p => p.EditOperations); - } - /// Assert that at most one loader will be applied to an asset. /// The basic asset metadata. /// The asset loaders to apply. /// The error message to show to the user, if the method returns false. /// Returns true if only one loader will apply, else false. - private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, [NotNullWhen(false)] out string? error) + private bool AssertMaxOneRequiredLoader(IAssetInfo info, List 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; } -- cgit