From a546fd113f431bd8888da50aad087213193c937e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 May 2022 18:02:48 -0400 Subject: add experimental image load rewrite --- .../Framework/ContentManagers/ModContentManager.cs | 41 +++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1b94b8c6..055dcc5f 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -7,6 +7,7 @@ using BmFont; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using SkiaSharp; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialization; @@ -25,6 +26,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Fields *********/ + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + private readonly bool UseExperimentalImageLoading; + /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; @@ -57,13 +61,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke when the content manager is being disposed. /// A lookup for files within the . - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup) + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup, bool useExperimentalImageLoading) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; this.JsonHelper = jsonHelper; this.ModName = modName; + this.UseExperimentalImageLoading = useExperimentalImageLoading; this.TryLocalizeKeys = false; } @@ -187,10 +193,35 @@ namespace StardewModdingAPI.Framework.ContentManagers 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); - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - return (T)(object)texture; + if (this.UseExperimentalImageLoading) + { + // load raw data + using FileStream stream = File.OpenRead(file.FullName); + using SKBitmap bitmap = SKBitmap.Decode(stream); + SKPMColor[] rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); + + // convert to XNA pixel format + Color[] pixels = new Color[rawPixels.Length]; + for (int i = pixels.Length - 1; i >= 0; i--) + { + SKPMColor pixel = rawPixels[i]; + pixels[i] = pixel.Alpha == 0 + ? Color.Transparent + : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); + } + + // create texture + Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height); + texture.SetData(pixels); + return (T)(object)texture; + } + else + { + using FileStream stream = File.OpenRead(file.FullName); + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + return (T)(object)texture; + } } /// Load an unpacked image file (.tbin or .tmx). -- cgit From 4708385f696d2e47d24e795f210752f4b0224bff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 May 2022 22:46:51 -0400 Subject: add IRawTextureData asset type --- docs/release-notes.md | 3 + src/SMAPI/Framework/Content/AssetDataForImage.cs | 173 ++++++++++++++------- src/SMAPI/Framework/Content/RawTextureData.cs | 10 ++ .../ContentManagers/GameContentManager.cs | 4 + .../Framework/ContentManagers/ModContentManager.cs | 23 ++- src/SMAPI/IAssetDataForImage.cs | 10 ++ src/SMAPI/IContentHelper.cs | 2 +- src/SMAPI/IContentPack.cs | 2 +- src/SMAPI/IModContentHelper.cs | 2 +- src/SMAPI/IRawTextureData.cs | 17 ++ 10 files changed, 185 insertions(+), 61 deletions(-) create mode 100644 src/SMAPI/Framework/Content/RawTextureData.cs create mode 100644 src/SMAPI/IRawTextureData.cs (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/docs/release-notes.md b/docs/release-notes.md index a1b5222e..b22f4de9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,9 @@ * For players: * Added experimental image load rewrite (disabled by default). _If you have many content mods installed, enabling `UseExperimentalImageLoading` in `smapi-internal/config.json` may reduce load times or stutters when they load many image files at once._ +* For mod authors: + * Added specialized `IRawTextureData` asset type. + _When you're only loading a mod file to patch it into an asset, you can now load it using `helper.ModContent.Load(path)`. This reads the image data from disk without initializing a `Texture2D` instance through the GPU. You can then pass this to SMAPI APIs that accept `Texture2D` instances._ * For mod authors: * Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players. diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 97729c95..3393b22f 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -29,86 +30,154 @@ namespace StardewModdingAPI.Framework.Content : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// - public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - // get texture + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + + // validate source data if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); - Texture2D target = this.Data; + throw new ArgumentNullException(nameof(source), "Can't patch from null source data."); - // get areas - sourceArea ??= new Rectangle(0, 0, source.Width, source.Height); - targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + // get the pixels for the source area + Color[] sourceData; + { + int areaX = sourceArea.Value.X; + int areaY = sourceArea.Value.Y; + int areaWidth = sourceArea.Value.Width; + int areaHeight = sourceArea.Value.Height; - // validate + if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height) + sourceData = source.Data; + else + { + sourceData = new Color[areaWidth * areaHeight]; + int i = 0; + for (int y = areaY, maxY = areaY + areaHeight - 1; y <= maxY; y++) + { + for (int x = areaX, maxX = areaX + areaWidth - 1; x <= maxX; x++) + { + int targetIndex = (y * source.Width) + x; + sourceData[i++] = source.Data[targetIndex]; + } + } + } + } + + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + + /// + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + + // validate source texture + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); if (!source.Bounds.Contains(sourceArea.Value)) throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (!target.Bounds.Contains(targetArea.Value)) - throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Size != targetArea.Value.Size) - throw new InvalidOperationException("The source and target areas must be the same size."); // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = GC.AllocateUninitializedArray(pixelCount); source.GetData(0, sourceArea, sourceData, 0, pixelCount); - // merge data in overlay mode + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + + /// + public bool ExtendImage(int minWidth, int minHeight) + { + if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) + return false; + + Texture2D original = this.Data; + Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); + this.ReplaceWith(texture); + this.PatchImage(original); + return true; + } + + + /********* + ** Private methods + *********/ + /// Get the bounds for an image patch. + /// The source area to set if needed. + /// The target area to set if needed. + /// The width of the full source image. + /// The height of the full source image. + private void GetPatchBounds([NotNull] ref Rectangle? sourceArea, [NotNull] ref Rectangle? targetArea, int sourceWidth, int sourceHeight) + { + sourceArea ??= new Rectangle(0, 0, sourceWidth, sourceHeight); + targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, this.Data.Width), Math.Min(sourceArea.Value.Height, this.Data.Height)); + } + + /// Overwrite part of the image. + /// The image data to patch into the content. + /// The pixel width of the source image. + /// The pixel height of the source image. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + /// The content being read isn't an image. + private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode) + { + // get texture + Texture2D target = this.Data; + int pixelCount = sourceArea.Width * sourceArea.Height; + + // validate + if (sourceArea.X < 0 || sourceArea.Y < 0 || sourceArea.Right > sourceWidth || sourceArea.Bottom > sourceHeight) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (!target.Bounds.Contains(targetArea)) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Size != targetArea.Size) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // merge data if (patchMode == PatchMode.Overlay) { // get target data - Color[] targetData = GC.AllocateUninitializedArray(pixelCount); - target.GetData(0, targetArea, targetData, 0, pixelCount); + Color[] mergedData = GC.AllocateUninitializedArray(pixelCount); + target.GetData(0, targetArea, mergedData, 0, pixelCount); // merge pixels - for (int i = 0; i < sourceData.Length; i++) + for (int i = 0; i < pixelCount; i++) { Color above = sourceData[i]; - Color below = targetData[i]; + Color below = mergedData[i]; // shortcut transparency if (above.A < MinOpacity) - { - sourceData[i] = below; continue; - } if (below.A < MinOpacity) - { - sourceData[i] = above; - continue; - } + mergedData[i] = above; // merge pixels - // This performs a conventional alpha blend for the pixels, which are already - // premultiplied by the content pipeline. The formula is derived from - // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. - // Note: don't use named arguments here since they're different between - // Linux/macOS and Windows. - float alphaBelow = 1 - (above.A / 255f); - sourceData[i] = new Color( - (int)(above.R + (below.R * alphaBelow)), // r - (int)(above.G + (below.G * alphaBelow)), // g - (int)(above.B + (below.B * alphaBelow)), // b - Math.Max(above.A, below.A) // a - ); + else + { + // This performs a conventional alpha blend for the pixels, which are already + // premultiplied by the content pipeline. The formula is derived from + // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + float alphaBelow = 1 - (above.A / 255f); + mergedData[i] = new Color( + r: (int)(above.R + (below.R * alphaBelow)), + g: (int)(above.G + (below.G * alphaBelow)), + b: (int)(above.B + (below.B * alphaBelow)), + alpha: Math.Max(above.A, below.A) + ); + } } - } - - // patch target texture - target.SetData(0, targetArea, sourceData, 0, pixelCount); - } - - /// - public bool ExtendImage(int minWidth, int minHeight) - { - if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) - return false; - Texture2D original = this.Data; - Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); - this.ReplaceWith(texture); - this.PatchImage(original); - return true; + target.SetData(0, targetArea, mergedData, 0, pixelCount); + } + else + target.SetData(0, targetArea, sourceData, 0, pixelCount); } } } diff --git a/src/SMAPI/Framework/Content/RawTextureData.cs b/src/SMAPI/Framework/Content/RawTextureData.cs new file mode 100644 index 00000000..4a0835b0 --- /dev/null +++ b/src/SMAPI/Framework/Content/RawTextureData.cs @@ -0,0 +1,10 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Framework.Content +{ + /// The raw data for an image read from the filesystem. + /// The image width. + /// The image height. + /// The loaded image data. + internal record RawTextureData(int Width, int Height, Color[] Data) : IRawTextureData; +} diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 4390d472..446f4a67 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -9,6 +9,7 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Deprecations; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; @@ -93,6 +94,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// public override T LoadExact(IAssetName assetName, bool useCache) { + if (typeof(IRawTextureData).IsAssignableFrom(typeof(T))) + throw new SContentLoadException(ContentLoadErrorType.Other, $"Can't load {nameof(IRawTextureData)} assets from the game content pipeline. This asset type is only available for mod files."); + // raise first-load callback if (GameContentManager.IsFirstLoad) { diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 055dcc5f..eb4f4555 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using SkiaSharp; +using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialization; @@ -188,12 +189,17 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file to load. private T LoadImageFile(IAssetName assetName, FileInfo file) { - // validate + // validate type + bool asRawData = false; if (typeof(T) != 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)}'."); + { + asRawData = typeof(T) == typeof(IRawTextureData); + if (!asRawData) + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}' or '{typeof(IRawTextureData)}'."); + } // load - if (this.UseExperimentalImageLoading) + if (asRawData || this.UseExperimentalImageLoading) { // load raw data using FileStream stream = File.OpenRead(file.FullName); @@ -211,9 +217,14 @@ namespace StardewModdingAPI.Framework.ContentManagers } // create texture - Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height); - texture.SetData(pixels); - return (T)(object)texture; + if (asRawData) + return (T)(object)new RawTextureData(bitmap.Width, bitmap.Height, pixels); + else + { + Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height); + texture.SetData(pixels); + return (T)(object)texture; + } } else { diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs index 6f8a4719..3e5b833d 100644 --- a/src/SMAPI/IAssetDataForImage.cs +++ b/src/SMAPI/IAssetDataForImage.cs @@ -10,6 +10,16 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + /// The content being read isn't an image. + void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); + /// Overwrite part of the image. /// The image to patch into the content. /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index 2cd0c1fc..7637edf0 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI ** Public methods *********/ /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , (for mod content only), and data structures; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. /// Where to search for a matching content asset. /// The is empty or contains invalid characters. diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 1215fe0b..73b1a860 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -48,7 +48,7 @@ namespace StardewModdingAPI where TModel : class; /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , , and data structures; other types may be supported by the game's content pipeline. /// The relative file path within the content pack (case-insensitive). /// The is empty or contains invalid characters. /// The content asset couldn't be loaded (e.g. because it doesn't exist). diff --git a/src/SMAPI/IModContentHelper.cs b/src/SMAPI/IModContentHelper.cs index f1f6ce94..1e2d82a8 100644 --- a/src/SMAPI/IModContentHelper.cs +++ b/src/SMAPI/IModContentHelper.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI ** Public methods *********/ /// Load content from the mod folder and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. + /// The expected data type. The main supported types are , , , and data structures; other types may be supported by the game's content pipeline. /// The local path to a content file relative to the mod folder. /// The is empty or contains invalid characters. /// The content asset couldn't be loaded (e.g. because it doesn't exist). diff --git a/src/SMAPI/IRawTextureData.cs b/src/SMAPI/IRawTextureData.cs new file mode 100644 index 00000000..a4da52f3 --- /dev/null +++ b/src/SMAPI/IRawTextureData.cs @@ -0,0 +1,17 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI +{ + /// The raw data for an image read from the filesystem. + public interface IRawTextureData + { + /// The image width. + int Width { get; } + + /// The image height. + int Height { get; } + + /// The loaded image data. + Color[] Data { get; } + } +} -- cgit From 769475166ab3b92cd3763bb86e364a8b2c7d914f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 May 2022 00:51:11 -0400 Subject: enable raw image loading by default, rename setting --- docs/release-notes.md | 7 +++---- src/SMAPI/Framework/ContentCoordinator.cs | 12 ++++++------ src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 12 ++++++------ src/SMAPI/Framework/Models/SConfig.cs | 12 ++++++------ src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/SMAPI.config.json | 5 +++-- 6 files changed, 25 insertions(+), 25 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/docs/release-notes.md b/docs/release-notes.md index b22f4de9..7fff656a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,11 +3,10 @@ # Release notes ## Upcoming release * For players: - * Added experimental image load rewrite (disabled by default). - _If you have many content mods installed, enabling `UseExperimentalImageLoading` in `smapi-internal/config.json` may reduce load times or stutters when they load many image files at once._ + * Optimized mod image file loading. * For mod authors: - * Added specialized `IRawTextureData` asset type. - _When you're only loading a mod file to patch it into an asset, you can now load it using `helper.ModContent.Load(path)`. This reads the image data from disk without initializing a `Texture2D` instance through the GPU. You can then pass this to SMAPI APIs that accept `Texture2D` instances._ + * Added a new `IRawTextureData` asset type. + _You can now load image files through `helper.ModContent` as `IRawTextureData` instead of `Texture2D`. This provides the image size and raw pixel data, which you can pass into other SMAPI APIs like `asset.AsImage().PatchImage`. This is much more efficient when you don't need a full `Texture2D` instance, since it bypasses the GPU operations needed to create one._ * For mod authors: * Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 3ad112cd..3e09ac62 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -32,8 +32,8 @@ namespace StardewModdingAPI.Framework /// An asset key prefix for assets from SMAPI mod folders. private readonly string ManagedPrefix = "SMAPI"; - /// Whether to use a newer approach when loading image files from mod folder which may be faster. - private readonly bool UseExperimentalImageLoading; + /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. + private readonly bool UseRawImageLoading; /// Get a file lookup for the given directory. private readonly Func GetFileLookup; @@ -133,8 +133,8 @@ namespace StardewModdingAPI.Framework /// Get a file lookup for the given directory. /// A callback to invoke when any asset names have been invalidated from the cache. /// Get the load/edit operations to apply to an asset by querying registered event handlers. - /// Whether to use a newer approach when loading image files from mod folder which may be faster. - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations, bool useExperimentalImageLoading) + /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations, bool useRawImageLoading) { this.GetFileLookup = getFileLookup; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Framework this.OnAssetsInvalidated = onAssetsInvalidated; this.RequestAssetOperations = requestAssetOperations; this.FullRootDirectory = Path.Combine(Constants.GamePath, rootDirectory); - this.UseExperimentalImageLoading = useExperimentalImageLoading; + this.UseRawImageLoading = useRawImageLoading; this.ContentManagers.Add( this.MainContentManager = new GameContentManager( name: "Game1.content", @@ -225,7 +225,7 @@ namespace StardewModdingAPI.Framework jsonHelper: this.JsonHelper, onDisposing: this.OnDisposing, fileLookup: this.GetFileLookup(rootDirectory), - useExperimentalImageLoading: this.UseExperimentalImageLoading + useRawImageLoading: this.UseRawImageLoading ); this.ContentManagers.Add(manager); return manager; diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index eb4f4555..f0e9b1b9 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -27,8 +27,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Fields *********/ - /// Whether to use a newer approach when loading image files from mod folder which may be faster. - private readonly bool UseExperimentalImageLoading; + /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. + private readonly bool UseRawImageLoading; /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; @@ -62,15 +62,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke when the content manager is being disposed. /// A lookup for files within the . - /// Whether to use a newer approach when loading image files from mod folder which may be faster. - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup, bool useExperimentalImageLoading) + /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup, bool useRawImageLoading) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; this.JsonHelper = jsonHelper; this.ModName = modName; - this.UseExperimentalImageLoading = useExperimentalImageLoading; + this.UseRawImageLoading = useRawImageLoading; this.TryLocalizeKeys = false; } @@ -199,7 +199,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // load - if (asRawData || this.UseExperimentalImageLoading) + if (asRawData || this.UseRawImageLoading) { // load raw data using FileStream stream = File.OpenRead(file.FullName); diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index f12da0a7..6edaa818 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.Models [nameof(LogNetworkTraffic)] = false, [nameof(RewriteMods)] = true, [nameof(UsePintail)] = true, - [nameof(UseExperimentalImageLoading)] = false, + [nameof(UseRawImageLoading)] = true, [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux }; @@ -67,8 +67,8 @@ namespace StardewModdingAPI.Framework.Models /// Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself. public bool UsePintail { get; } - /// Whether to use a newer approach when loading image files from mod folder which may be faster. - public bool UseExperimentalImageLoading { get; } + /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. + public bool UseRawImageLoading { get; } /// Whether to make SMAPI file APIs case-insensitive, even on Linux. public bool UseCaseInsensitivePaths { get; } @@ -96,12 +96,12 @@ namespace StardewModdingAPI.Framework.Models /// The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting. /// Whether SMAPI should rewrite mods for compatibility. /// Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself. - /// Whether to use a newer approach when loading image files from mod folder which may be faster. + /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. /// >Whether to make SMAPI file APIs case-insensitive, even on Linux. /// Whether SMAPI should log network traffic. /// The colors to use for text written to the SMAPI console. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useExperimentalImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -112,7 +112,7 @@ namespace StardewModdingAPI.Framework.Models this.VerboseLogging = new HashSet(verboseLogging ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)]; this.UsePintail = usePintail ?? (bool)SConfig.DefaultValues[nameof(this.UsePintail)]; - this.UseExperimentalImageLoading = useExperimentalImageLoading ?? (bool)SConfig.DefaultValues[nameof(this.UseExperimentalImageLoading)]; + this.UseRawImageLoading = useRawImageLoading ?? (bool)SConfig.DefaultValues[nameof(this.UseRawImageLoading)]; this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(this.UseCaseInsensitivePaths)]; this.LogNetworkTraffic = logNetworkTraffic ?? (bool)SConfig.DefaultValues[nameof(this.LogNetworkTraffic)]; this.ConsoleColors = consoleColors; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 242776b3..f018acad 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1302,7 +1302,7 @@ namespace StardewModdingAPI.Framework onAssetsInvalidated: this.OnAssetsInvalidated, getFileLookup: this.GetFileLookup, requestAssetOperations: this.RequestAssetOperations, - useExperimentalImageLoading: this.Settings.UseExperimentalImageLoading + useRawImageLoading: this.Settings.UseRawImageLoading ); if (this.ContentCore.Language != this.Translator.LocaleEnum) this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 8e710435..97e8e00c 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -61,9 +61,10 @@ copy all the settings, or you may cause bugs due to overridden changes in future "UsePintail": true, /** - * Whether to use a newer approach when loading image files from mod folder which may be faster. + * Whether to use raw image data when possible, instead of initializing an XNA Texture2D + * instance through the GPU. */ - "UseExperimentalImageLoading": false, + "UseRawImageLoading": true, /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially -- cgit From 5ffa260e02fc8a6eae40c1fd798da2a4877262c0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 26 May 2022 01:41:49 -0400 Subject: add validation error when loading XNB file as IRawTextureData --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index f0e9b1b9..44c9d8e4 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -258,6 +258,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset name relative to the loader root directory. private T LoadXnbFile(IAssetName assetName) { + if (typeof(T) == typeof(IRawTextureData)) + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file."); + // the underlying content manager adds a .xnb extension implicitly, so // we need to strip it here to avoid trying to load a '.xnb.xnb' file. IAssetName loadName = assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase) -- cgit From 4f6965eef3c7690dcfc1e1946362a84f68f40767 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 May 2022 15:07:19 -0400 Subject: encapsulate loading the raw image data for mod patching --- .../Framework/ContentManagers/ModContentManager.cs | 53 +++++++++++++++------- 1 file changed, 36 insertions(+), 17 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 44c9d8e4..fb031ccb 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -201,27 +202,13 @@ namespace StardewModdingAPI.Framework.ContentManagers // load if (asRawData || this.UseRawImageLoading) { - // load raw data - using FileStream stream = File.OpenRead(file.FullName); - using SKBitmap bitmap = SKBitmap.Decode(stream); - SKPMColor[] rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); - - // convert to XNA pixel format - Color[] pixels = new Color[rawPixels.Length]; - for (int i = pixels.Length - 1; i >= 0; i--) - { - SKPMColor pixel = rawPixels[i]; - pixels[i] = pixel.Alpha == 0 - ? Color.Transparent - : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); - } + this.LoadRawImageData(file, out int width, out int height, out Color[] pixels, asRawData); - // create texture if (asRawData) - return (T)(object)new RawTextureData(bitmap.Width, bitmap.Height, pixels); + return (T)(object)new RawTextureData(width, height, pixels); else { - Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height); + Texture2D texture = new(Game1.graphics.GraphicsDevice, width, height); texture.SetData(pixels); return (T)(object)texture; } @@ -235,6 +222,38 @@ namespace StardewModdingAPI.Framework.ContentManagers } } + /// Load the raw image data from a file on disk. + /// The file whose data to load. + /// The pixel width for the loaded image data. + /// The pixel height for the loaded image data. + /// The premultiplied pixel data. + /// Whether the data is being loaded for an (true) or (false) instance. + /// This is separate to let framework mods intercept the data before it's loaded, if needed. + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] + private void LoadRawImageData(FileInfo file, out int width, out int height, out Color[] pixels, bool forRawData) + { + // load raw data + SKPMColor[] rawPixels; + { + using FileStream stream = File.OpenRead(file.FullName); + using SKBitmap bitmap = SKBitmap.Decode(stream); + rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); + width = bitmap.Width; + height = bitmap.Height; + } + + // convert to XNA pixel format + pixels = new Color[rawPixels.Length]; + for (int i = pixels.Length - 1; i >= 0; i--) + { + SKPMColor pixel = rawPixels[i]; + pixels[i] = pixel.Alpha == 0 + ? Color.Transparent + : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); + } + } + /// Load an unpacked image file (.tbin or .tmx). /// The type of asset to load. /// The asset name relative to the loader root directory. -- cgit From 9d21e0bbecb64243cf98e6c9a10ed5aa4ceed2bf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 May 2022 19:17:33 -0400 Subject: simplify pixel conversion loop --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index fb031ccb..e1d9ce78 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -245,7 +245,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // convert to XNA pixel format pixels = new Color[rawPixels.Length]; - for (int i = pixels.Length - 1; i >= 0; i--) + for (int i = 0; i < pixels.Length; i++) { SKPMColor pixel = rawPixels[i]; pixels[i] = pixel.Alpha == 0 -- cgit From 5585f5e876459e1764e44754b5b7d933fa1456de Mon Sep 17 00:00:00 2001 From: Ameisen <14104310+ameisen@users.noreply.github.com> Date: Sun, 29 May 2022 18:11:23 -0500 Subject: Refactored ModContentManager.cs so it actually fit on my 1440p screens. Changed LocalTilesheetExtensions into an array. Marked 'CreateTemporary' as 'Obsolete' which is conventional for methods that only throw. Moved the type validation logic into its own method as it's largely shared for each loader. Changed allocators to use `GC.AllocateUninitializedArray`, as the data does not need to be initialized. Changed `LoadRawImageData` to use a `ValueTuple` return instead of returning with multiple `out`s, which is bad practice. Preferred rethrowing handlers rather than exception filters (which generate bizarre and _very difficult to patch_ code). Marked GetLoadError as debugger step through and hidden, as it's just an exception generator. Marked PremultiplyTransparency, GetContentKeyForTilesheetImageSource, and LoadRawImageData as static as they have no dependency on instance data (nor should they). Fixed `.xnb` extension search to properly use OrdinalIgnoreCase. --- .../Framework/ContentManagers/ModContentManager.cs | 239 ++++++++++++++++----- 1 file changed, 183 insertions(+), 56 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index e1d9ce78..fe5aaf5d 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,11 +1,11 @@ using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using BmFont; -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using SkiaSharp; @@ -19,11 +19,12 @@ using StardewValley; using xTile; using xTile.Format; using xTile.Tiles; +using Color = Microsoft.Xna.Framework.Color; namespace StardewModdingAPI.Framework.ContentManagers { /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. - internal class ModContentManager : BaseContentManager + internal sealed class ModContentManager : BaseContentManager { /********* ** Fields @@ -44,7 +45,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private readonly IFileLookup FileLookup; /// If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder. - private static readonly HashSet LocalTilesheetExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".xnb" }; + private static readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" }; /********* @@ -64,8 +65,21 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A callback to invoke when the content manager is being disposed. /// A lookup for files within the . /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup, bool useRawImageLoading) - : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) + public ModContentManager( + string name, + IContentManager gameContentManager, + IServiceProvider serviceProvider, + string modName, + string rootDirectory, + CultureInfo currentCulture, + ContentCoordinator coordinator, + IMonitor monitor, + Reflector reflection, + JsonHelper jsonHelper, + Action onDisposing, + IFileLookup fileLookup, + bool useRawImageLoading + ) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; @@ -102,7 +116,14 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath)) { if (contentManagerID != this.Name) - throw this.GetLoadError(assetName, ContentLoadErrorType.AccessDenied, "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; } } @@ -127,7 +148,11 @@ namespace StardewModdingAPI.Framework.ContentManagers _ => this.HandleUnknownFileType(assetName, file) }; } - catch (Exception ex) when (ex is not SContentLoadException) + catch (SContentLoadException) + { + throw; + } + catch (Exception ex) { throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex); } @@ -138,6 +163,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } /// + [Obsolete($"Temporary {nameof(ModContentManager)}s are unsupported")] public override LocalizedContentManager CreateTemporary() { throw new NotSupportedException("Can't create a temporary mod content manager."); @@ -157,6 +183,67 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ + /// + /// Validates that the provided type is compatible with . + /// + /// Type to validate compatibility of. + /// Type to validate compatibility against. + /// The asset name relative to the loader root directory. + /// The file being loaded. + /// The exception to throw if the type validation fails, otherwise . + /// if the type validation succeeds, otherwise + private bool ValidateType( + IAssetName assetName, + FileInfo file, + [NotNullWhen(false)] out SContentLoadException? exception + ) + { + if (typeof(TInput).IsAssignableFrom(typeof(TExpected))) + { + exception = null; + return true; + } + + exception = this.GetLoadError( + assetName, + ContentLoadErrorType.InvalidData, + $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected)}'." + ); + return false; + } + + /// + /// Validates that the provided type + /// is compatible with or + /// + /// Type to validate compatibility of. + /// First type to validate compatibility against. + /// /// Second type to validate compatibility against. + /// The asset name relative to the loader root directory. + /// The file being loaded. + /// The exception to throw if the type validation fails, otherwise . + /// if the type validation succeeds, otherwise + private bool ValidateType( + IAssetName assetName, + FileInfo file, + [NotNullWhen(false)] out SContentLoadException? exception + ) + { + if (typeof(TInput).IsAssignableFrom(typeof(TExpected0)) || typeof(TInput).IsAssignableFrom(typeof(TExpected1))) + { + exception = null; + return true; + } + + exception = this.GetLoadError( + assetName, + ContentLoadErrorType.InvalidData, + $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected0)}' or '{typeof(TExpected1)}'." + ); + return false; + } + + /// Load an unpacked font file (.fnt). /// The type of asset to load. /// The asset name relative to the loader root directory. @@ -164,8 +251,10 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadFont(IAssetName assetName, FileInfo file) { // validate - if (!typeof(T).IsAssignableFrom(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)}'."); + if (!this.ValidateType(assetName, file, out var exception)) + { + throw exception; + } // load string source = File.ReadAllText(file.FullName); @@ -179,7 +268,10 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadDataFile(IAssetName assetName, FileInfo file) { if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset)) - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method + { + // should never happen as we check for file existence before calling this method + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); + } return asset; } @@ -191,24 +283,23 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadImageFile(IAssetName assetName, FileInfo file) { // validate type - bool asRawData = false; - if (typeof(T) != typeof(Texture2D)) + if (!this.ValidateType(assetName, file, out var exception)) { - asRawData = typeof(T) == typeof(IRawTextureData); - if (!asRawData) - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}' or '{typeof(IRawTextureData)}'."); + throw exception; } + bool asRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); + // load if (asRawData || this.UseRawImageLoading) { - this.LoadRawImageData(file, out int width, out int height, out Color[] pixels, asRawData); + (Size size, Color[] pixels) = ModContentManager.LoadRawImageData(file, asRawData); if (asRawData) - return (T)(object)new RawTextureData(width, height, pixels); + return (T)(object)new RawTextureData(size.Width, size.Height, pixels); else { - Texture2D texture = new(Game1.graphics.GraphicsDevice, width, height); + Texture2D texture = new(Game1.graphics.GraphicsDevice, size.Width, size.Height); texture.SetData(pixels); return (T)(object)texture; } @@ -217,34 +308,32 @@ namespace StardewModdingAPI.Framework.ContentManagers { using FileStream stream = File.OpenRead(file.FullName); Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); + texture = ModContentManager.PremultiplyTransparency(texture); return (T)(object)texture; } } /// Load the raw image data from a file on disk. /// The file whose data to load. - /// The pixel width for the loaded image data. - /// The pixel height for the loaded image data. - /// The premultiplied pixel data. /// Whether the data is being loaded for an (true) or (false) instance. /// This is separate to let framework mods intercept the data before it's loaded, if needed. [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] - private void LoadRawImageData(FileInfo file, out int width, out int height, out Color[] pixels, bool forRawData) + private static (Size Size, Color[] Data) LoadRawImageData(FileInfo file, bool forRawData) { + Size size; + // load raw data SKPMColor[] rawPixels; { using FileStream stream = File.OpenRead(file.FullName); using SKBitmap bitmap = SKBitmap.Decode(stream); rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); - width = bitmap.Width; - height = bitmap.Height; + size = new(bitmap.Width, bitmap.Height); } // convert to XNA pixel format - pixels = new Color[rawPixels.Length]; + var pixels = GC.AllocateUninitializedArray(rawPixels.Length); for (int i = 0; i < pixels.Length; i++) { SKPMColor pixel = rawPixels[i]; @@ -252,6 +341,8 @@ namespace StardewModdingAPI.Framework.ContentManagers ? Color.Transparent : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); } + + return (size, pixels); } /// Load an unpacked image file (.tbin or .tmx). @@ -261,8 +352,10 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadMapFile(IAssetName assetName, FileInfo file) { // validate - if (typeof(T) != 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)}'."); + if (!this.ValidateType(assetName, file, out var exception)) + { + throw exception; + } // load FormatManager formatManager = FormatManager.Instance; @@ -277,8 +370,14 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset name relative to the loader root directory. private T LoadXnbFile(IAssetName assetName) { - if (typeof(T) == typeof(IRawTextureData)) - throw this.GetLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file."); + if (typeof(IRawTextureData).IsAssignableFrom(typeof(T))) + { + throw this.GetLoadError( + assetName, + ContentLoadErrorType.Other, + $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file." + ); + } // the underlying content manager adds a .xnb extension implicitly, so // we need to strip it here to avoid trying to load a '.xnb.xnb' file. @@ -303,7 +402,11 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file to load. private T HandleUnknownFileType(IAssetName assetName, FileInfo file) { - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"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. @@ -311,6 +414,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset name that failed to load. /// The reason the file couldn't be loaded. /// The underlying exception, if applicable. + [DebuggerStepThrough, DebuggerHidden] private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); @@ -325,16 +429,16 @@ namespace StardewModdingAPI.Framework.ContentManagers FileInfo file = this.FileLookup.GetFile(path); // try with default image extensions - if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension)) + if (file.Exists || !typeof(Texture2D).IsAssignableFrom(typeof(T)) || ModContentManager.LocalTilesheetExtensions.Contains(file.Extension)) + return file; + + foreach (string extension in ModContentManager.LocalTilesheetExtensions) { - foreach (string extension in ModContentManager.LocalTilesheetExtensions) + FileInfo result = new(file.FullName + extension); + if (result.Exists) { - FileInfo result = new(file.FullName + extension); - if (result.Exists) - { - file = result; - break; - } + file = result; + break; } } @@ -345,10 +449,10 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The texture to premultiply. /// Returns a premultiplied texture. /// Based on code by David Gouveia. - private Texture2D PremultiplyTransparency(Texture2D texture) + private static Texture2D PremultiplyTransparency(Texture2D texture) { // premultiply pixels - Color[] data = new Color[texture.Width * texture.Height]; + Color[] data = GC.AllocateUninitializedArray(texture.Width * texture.Height); texture.GetData(data); bool changed = false; for (int i = 0; i < data.Length; i++) @@ -357,7 +461,12 @@ namespace StardewModdingAPI.Framework.ContentManagers if (pixel.A is (byte.MinValue or byte.MaxValue)) continue; // no need to change fully transparent/opaque pixels - data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) + data[i] = new Color( + pixel.R * pixel.A / byte.MaxValue, + pixel.G * pixel.A / byte.MaxValue, + pixel.B * pixel.A / byte.MaxValue, + pixel.A + ); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) changed = true; } @@ -370,7 +479,10 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. /// The relative map path within the mod folder. - /// Whether to undo the game's eager tilesheet path prefixing for maps loaded from an .xnb file, which incorrectly prefixes tilesheet paths with the map's local asset key folder. + /// + /// Whether to undo the game's eager tilesheet path prefixing for maps loaded from an .xnb file, + /// which incorrectly prefixes tilesheet paths with the map's local asset key folder. + /// /// A map tilesheet couldn't be resolved. private void FixTilesheetPaths(Map map, string relativeMapPath, bool fixEagerPathPrefixes) { @@ -388,18 +500,28 @@ namespace StardewModdingAPI.Framework.ContentManagers // reverse incorrect eager tilesheet path prefixing if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource.StartsWith(relativeMapFolder)) - imageSource = imageSource.Substring(relativeMapFolder.Length + 1); + imageSource = imageSource[(relativeMapFolder.Length + 1)..]; // 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(ContentLoadErrorType.InvalidData, $"{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(ContentLoadErrorType.InvalidData, $"{errorPrefix} {error}"); + { + throw new SContentLoadException( + ContentLoadErrorType.InvalidData, + $"{errorPrefix} {error}" + ); + } if (assetName is not null) { @@ -409,7 +531,11 @@ namespace StardewModdingAPI.Framework.ContentManagers tilesheet.ImageSource = assetName.Name; } } - catch (Exception ex) when (ex is not SContentLoadException) + catch (SContentLoadException) + { + throw; + } + catch (Exception ex) { throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex); } @@ -425,7 +551,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// See remarks on . private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, out string? error) { - assetName = null; error = null; // nothing to do @@ -440,7 +565,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // opened in Tiled, while still mapping it to the vanilla 'Maps/spring_town' asset at runtime. { string filename = Path.GetFileName(relativePath); - if (filename.StartsWith(".")) + if (filename.StartsWith('.')) relativePath = Path.Combine(Path.GetDirectoryName(relativePath) ?? "", filename.TrimStart('.')); } @@ -455,10 +580,11 @@ namespace StardewModdingAPI.Framework.ContentManagers } // get from game assets - IAssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false); + AssetName contentKey = this.Coordinator.ParseAssetName(ModContentManager.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false); try { - this.GameContentManager.LoadLocalized(contentKey, this.GameContentManager.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset + // no need to bypass cache here, since we're not storing the asset + this.GameContentManager.LoadLocalized(contentKey, this.GameContentManager.Language, useCache: true); assetName = contentKey; return true; } @@ -476,6 +602,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // not found + assetName = null; error = "The tilesheet couldn't be found relative to either map file or the game's content folder."; return false; } @@ -486,16 +613,16 @@ namespace StardewModdingAPI.Framework.ContentManagers { // get file path string path = Path.Combine(this.GameContentManager.FullRootDirectory, key); - if (!path.EndsWith(".xnb")) + if (!path.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)) path += ".xnb"; // get file - return new FileInfo(path).Exists; + return File.Exists(path); } /// Get the asset key for a tilesheet in the game's Maps content folder. /// The tilesheet image source. - private string GetContentKeyForTilesheetImageSource(string relativePath) + private static string GetContentKeyForTilesheetImageSource(string relativePath) { string key = relativePath; string topFolder = PathUtilities.GetSegments(key, limit: 2)[0]; @@ -506,7 +633,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // remove file extension from unpacked file if (key.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - key = key.Substring(0, key.Length - 4); + key = key[..^4]; return key; } -- cgit From 87c7095e8d7bfc1c1819e7d17cfea3d07c65dfea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 May 2022 01:22:50 -0400 Subject: apply style conventions --- .../Framework/ContentManagers/ModContentManager.cs | 155 +++++---------------- 1 file changed, 37 insertions(+), 118 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index fe5aaf5d..d4a30f71 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -65,21 +65,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A callback to invoke when the content manager is being disposed. /// A lookup for files within the . /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. - public ModContentManager( - string name, - IContentManager gameContentManager, - IServiceProvider serviceProvider, - string modName, - string rootDirectory, - CultureInfo currentCulture, - ContentCoordinator coordinator, - IMonitor monitor, - Reflector reflection, - JsonHelper jsonHelper, - Action onDisposing, - IFileLookup fileLookup, - bool useRawImageLoading - ) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup, bool useRawImageLoading) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; @@ -116,14 +103,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, - ContentLoadErrorType.AccessDenied, - "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; } } @@ -148,12 +128,11 @@ namespace StardewModdingAPI.Framework.ContentManagers _ => this.HandleUnknownFileType(assetName, file) }; } - catch (SContentLoadException) - { - throw; - } catch (Exception ex) { + if (ex is SContentLoadException) + throw; + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex); } @@ -183,20 +162,14 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// - /// Validates that the provided type is compatible with . - /// + /// Validates that the provided type is compatible with . /// Type to validate compatibility of. /// Type to validate compatibility against. /// The asset name relative to the loader root directory. /// The file being loaded. /// The exception to throw if the type validation fails, otherwise . /// if the type validation succeeds, otherwise - private bool ValidateType( - IAssetName assetName, - FileInfo file, - [NotNullWhen(false)] out SContentLoadException? exception - ) + private bool ValidateType(IAssetName assetName, FileInfo file, [NotNullWhen(false)] out SContentLoadException? exception) { if (typeof(TInput).IsAssignableFrom(typeof(TExpected))) { @@ -204,18 +177,11 @@ namespace StardewModdingAPI.Framework.ContentManagers return true; } - exception = this.GetLoadError( - assetName, - ContentLoadErrorType.InvalidData, - $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected)}'." - ); + exception = this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected)}'."); return false; } - /// - /// Validates that the provided type - /// is compatible with or - /// + /// Validates that the provided type is compatible with or . /// Type to validate compatibility of. /// First type to validate compatibility against. /// /// Second type to validate compatibility against. @@ -223,11 +189,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file being loaded. /// The exception to throw if the type validation fails, otherwise . /// if the type validation succeeds, otherwise - private bool ValidateType( - IAssetName assetName, - FileInfo file, - [NotNullWhen(false)] out SContentLoadException? exception - ) + private bool ValidateType(IAssetName assetName, FileInfo file, [NotNullWhen(false)] out SContentLoadException? exception) { if (typeof(TInput).IsAssignableFrom(typeof(TExpected0)) || typeof(TInput).IsAssignableFrom(typeof(TExpected1))) { @@ -235,11 +197,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return true; } - exception = this.GetLoadError( - assetName, - ContentLoadErrorType.InvalidData, - $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected0)}' or '{typeof(TExpected1)}'." - ); + exception = this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected0)}' or '{typeof(TExpected1)}'."); return false; } @@ -252,9 +210,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (!this.ValidateType(assetName, file, out var exception)) - { throw exception; - } // load string source = File.ReadAllText(file.FullName); @@ -268,10 +224,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadDataFile(IAssetName assetName, FileInfo file) { if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset)) - { - // should never happen as we check for file existence before calling this method - throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); - } + 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; } @@ -284,16 +237,14 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate type if (!this.ValidateType(assetName, file, out var exception)) - { throw exception; - } bool asRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); // load if (asRawData || this.UseRawImageLoading) { - (Size size, Color[] pixels) = ModContentManager.LoadRawImageData(file, asRawData); + (Size size, Color[] pixels) = this.LoadRawImageData(file, asRawData); if (asRawData) return (T)(object)new RawTextureData(size.Width, size.Height, pixels); @@ -308,7 +259,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { using FileStream stream = File.OpenRead(file.FullName); Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = ModContentManager.PremultiplyTransparency(texture); + texture = this.PremultiplyTransparency(texture); return (T)(object)texture; } } @@ -319,7 +270,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// This is separate to let framework mods intercept the data before it's loaded, if needed. [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] - private static (Size Size, Color[] Data) LoadRawImageData(FileInfo file, bool forRawData) + private (Size Size, Color[] Data) LoadRawImageData(FileInfo file, bool forRawData) { Size size; @@ -353,9 +304,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (!this.ValidateType(assetName, file, out var exception)) - { throw exception; - } // load FormatManager formatManager = FormatManager.Instance; @@ -371,13 +320,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadXnbFile(IAssetName assetName) { if (typeof(IRawTextureData).IsAssignableFrom(typeof(T))) - { - throw this.GetLoadError( - assetName, - ContentLoadErrorType.Other, - $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file." - ); - } + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file."); // the underlying content manager adds a .xnb extension implicitly, so // we need to strip it here to avoid trying to load a '.xnb.xnb' file. @@ -402,11 +345,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file to load. private T HandleUnknownFileType(IAssetName assetName, FileInfo file) { - throw this.GetLoadError( - assetName, - ContentLoadErrorType.InvalidName, - $"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. @@ -429,16 +368,16 @@ namespace StardewModdingAPI.Framework.ContentManagers FileInfo file = this.FileLookup.GetFile(path); // try with default image extensions - if (file.Exists || !typeof(Texture2D).IsAssignableFrom(typeof(T)) || ModContentManager.LocalTilesheetExtensions.Contains(file.Extension)) - return file; - - foreach (string extension in ModContentManager.LocalTilesheetExtensions) + if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension)) { - FileInfo result = new(file.FullName + extension); - if (result.Exists) + foreach (string extension in ModContentManager.LocalTilesheetExtensions) { - file = result; - break; + FileInfo result = new(file.FullName + extension); + if (result.Exists) + { + file = result; + break; + } } } @@ -449,7 +388,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The texture to premultiply. /// Returns a premultiplied texture. /// Based on code by David Gouveia. - private static Texture2D PremultiplyTransparency(Texture2D texture) + private Texture2D PremultiplyTransparency(Texture2D texture) { // premultiply pixels Color[] data = GC.AllocateUninitializedArray(texture.Width * texture.Height); @@ -461,12 +400,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (pixel.A is (byte.MinValue or byte.MaxValue)) continue; // no need to change fully transparent/opaque pixels - data[i] = new Color( - pixel.R * pixel.A / byte.MaxValue, - pixel.G * pixel.A / byte.MaxValue, - pixel.B * pixel.A / byte.MaxValue, - pixel.A - ); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) + data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) changed = true; } @@ -479,10 +413,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. /// The relative map path within the mod folder. - /// - /// Whether to undo the game's eager tilesheet path prefixing for maps loaded from an .xnb file, - /// which incorrectly prefixes tilesheet paths with the map's local asset key folder. - /// + /// Whether to undo the game's eager tilesheet path prefixing for maps loaded from an .xnb file, which incorrectly prefixes tilesheet paths with the map's local asset key folder. /// A map tilesheet couldn't be resolved. private void FixTilesheetPaths(Map map, string relativeMapPath, bool fixEagerPathPrefixes) { @@ -505,23 +436,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( - ContentLoadErrorType.InvalidData, - $"{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( - ContentLoadErrorType.InvalidData, - $"{errorPrefix} {error}" - ); - } + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} {error}"); if (assetName is not null) { @@ -531,12 +452,11 @@ namespace StardewModdingAPI.Framework.ContentManagers tilesheet.ImageSource = assetName.Name; } } - catch (SContentLoadException) - { - throw; - } catch (Exception ex) { + if (ex is SContentLoadException) + throw; + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex); } } @@ -580,11 +500,10 @@ namespace StardewModdingAPI.Framework.ContentManagers } // get from game assets - AssetName contentKey = this.Coordinator.ParseAssetName(ModContentManager.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false); + AssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false); try { - // no need to bypass cache here, since we're not storing the asset - this.GameContentManager.LoadLocalized(contentKey, this.GameContentManager.Language, useCache: true); + this.GameContentManager.LoadLocalized(contentKey, this.GameContentManager.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset assetName = contentKey; return true; } @@ -622,7 +541,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Get the asset key for a tilesheet in the game's Maps content folder. /// The tilesheet image source. - private static string GetContentKeyForTilesheetImageSource(string relativePath) + private string GetContentKeyForTilesheetImageSource(string relativePath) { string key = relativePath; string topFolder = PathUtilities.GetSegments(key, limit: 2)[0]; -- cgit From ba7f5701def491f66b3e14aa5990eba521dfcf7c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 May 2022 01:22:50 -0400 Subject: simplify asset type validaiton --- .../Framework/ContentManagers/ModContentManager.cs | 67 +++++----------------- 1 file changed, 15 insertions(+), 52 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index d4a30f71..1f38b76b 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -162,57 +162,14 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// Validates that the provided type is compatible with . - /// Type to validate compatibility of. - /// Type to validate compatibility against. - /// The asset name relative to the loader root directory. - /// The file being loaded. - /// The exception to throw if the type validation fails, otherwise . - /// if the type validation succeeds, otherwise - private bool ValidateType(IAssetName assetName, FileInfo file, [NotNullWhen(false)] out SContentLoadException? exception) - { - if (typeof(TInput).IsAssignableFrom(typeof(TExpected))) - { - exception = null; - return true; - } - - exception = this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected)}'."); - return false; - } - - /// Validates that the provided type is compatible with or . - /// Type to validate compatibility of. - /// First type to validate compatibility against. - /// /// Second type to validate compatibility against. - /// The asset name relative to the loader root directory. - /// The file being loaded. - /// The exception to throw if the type validation fails, otherwise . - /// if the type validation succeeds, otherwise - private bool ValidateType(IAssetName assetName, FileInfo file, [NotNullWhen(false)] out SContentLoadException? exception) - { - if (typeof(TInput).IsAssignableFrom(typeof(TExpected0)) || typeof(TInput).IsAssignableFrom(typeof(TExpected1))) - { - exception = null; - return true; - } - - exception = this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TInput)}'; must be type '{typeof(TExpected0)}' or '{typeof(TExpected1)}'."); - return false; - } - - /// Load an unpacked font file (.fnt). /// The type of asset to load. /// The asset name relative to the loader root directory. /// The file to load. private T LoadFont(IAssetName assetName, FileInfo file) { - // validate - if (!this.ValidateType(assetName, file, out var exception)) - throw exception; + this.AssertValidType(assetName, file, typeof(XmlSource)); - // load string source = File.ReadAllText(file.FullName); return (T)(object)new XmlSource(source); } @@ -235,10 +192,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file to load. private T LoadImageFile(IAssetName assetName, FileInfo file) { - // validate type - if (!this.ValidateType(assetName, file, out var exception)) - throw exception; - + this.AssertValidType(assetName, file, typeof(Texture2D), typeof(IRawTextureData)); bool asRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); // load @@ -302,11 +256,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file to load. private T LoadMapFile(IAssetName assetName, FileInfo file) { - // validate - if (!this.ValidateType(assetName, file, out var exception)) - throw exception; + this.AssertValidType(assetName, file, typeof(Map)); - // load FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); map.assetPath = assetName.Name; @@ -348,6 +299,18 @@ namespace StardewModdingAPI.Framework.ContentManagers throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); } + /// Assert that the asset type is compatible with one of the allowed types. + /// The actual asset type. + /// The asset name relative to the loader root directory. + /// The file being loaded. + /// The allowed asset types. + /// The is not compatible with any of the . + private void AssertValidType(IAssetName assetName, FileInfo file, params Type[] validTypes) + { + if (!validTypes.Any(validType => validType.IsAssignableFrom(typeof(TAsset)))) + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TAsset)}'; must be type '{string.Join("' or '", validTypes.Select(p => p.FullName))}'."); + } + /// 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. -- cgit From 565677c18ca4e12a2d8635b4ea2db49255d28142 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 May 2022 01:22:51 -0400 Subject: use IRawTextureData instead of intermediate tuple --- .../Framework/ContentManagers/ModContentManager.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1f38b76b..160e3c19 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,11 +1,11 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using BmFont; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using SkiaSharp; @@ -19,7 +19,6 @@ using StardewValley; using xTile; using xTile.Format; using xTile.Tiles; -using Color = Microsoft.Xna.Framework.Color; namespace StardewModdingAPI.Framework.ContentManagers { @@ -198,14 +197,14 @@ namespace StardewModdingAPI.Framework.ContentManagers // load if (asRawData || this.UseRawImageLoading) { - (Size size, Color[] pixels) = this.LoadRawImageData(file, asRawData); + IRawTextureData raw = this.LoadRawImageData(file, asRawData); if (asRawData) - return (T)(object)new RawTextureData(size.Width, size.Height, pixels); + return (T)raw; else { - Texture2D texture = new(Game1.graphics.GraphicsDevice, size.Width, size.Height); - texture.SetData(pixels); + Texture2D texture = new(Game1.graphics.GraphicsDevice, raw.Width, raw.Height); + texture.SetData(raw.Data); return (T)(object)texture; } } @@ -224,17 +223,18 @@ namespace StardewModdingAPI.Framework.ContentManagers /// This is separate to let framework mods intercept the data before it's loaded, if needed. [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")] - private (Size Size, Color[] Data) LoadRawImageData(FileInfo file, bool forRawData) + private IRawTextureData LoadRawImageData(FileInfo file, bool forRawData) { - Size size; - // load raw data + int width; + int height; SKPMColor[] rawPixels; { using FileStream stream = File.OpenRead(file.FullName); using SKBitmap bitmap = SKBitmap.Decode(stream); rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); - size = new(bitmap.Width, bitmap.Height); + width = bitmap.Width; + height = bitmap.Height; } // convert to XNA pixel format @@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework.ContentManagers : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); } - return (size, pixels); + return new RawTextureData(width, height, pixels); } /// Load an unpacked image file (.tbin or .tmx). -- cgit From 43e9e2cfe2c6938121d9a5e106acfc944a1821c6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 May 2022 01:30:43 -0400 Subject: fix accidental case-sensitivity change --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 160e3c19..8e2d58a6 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -331,7 +331,7 @@ namespace StardewModdingAPI.Framework.ContentManagers FileInfo file = this.FileLookup.GetFile(path); // try with default image extensions - if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension)) + if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension, StringComparer.OrdinalIgnoreCase)) { foreach (string extension in ModContentManager.LocalTilesheetExtensions) { -- cgit From 0189b282f4db300c70eeef5e9f714aa450deafde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 10 Jun 2022 22:14:28 -0400 Subject: add image compatibility mode for PyTK --- .../Framework/ContentManagers/ModContentManager.cs | 45 ++++++++++++++++++++-- src/SMAPI/Framework/SCore.cs | 6 +++ 2 files changed, 47 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 8e2d58a6..8c5d0f84 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -46,6 +47,16 @@ namespace StardewModdingAPI.Framework.ContentManagers /// If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder. private static readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" }; + /// A lookup of image file paths to whether they have PyTK scaling information. + private static readonly Dictionary IsPyTkScaled = new(StringComparer.OrdinalIgnoreCase); + + + /********* + ** Accessors + *********/ + /// Whether to enable legacy compatibility mode for PyTK scale-up textures. + internal static bool EnablePyTkLegacyMode; + /********* ** Public methods @@ -192,14 +203,40 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadImageFile(IAssetName assetName, FileInfo file) { this.AssertValidType(assetName, file, typeof(Texture2D), typeof(IRawTextureData)); - bool asRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); + bool expectsRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); + bool asRawData = expectsRawData || this.UseRawImageLoading; + + // disable raw data if PyTK will rescale the image (until it supports raw data) + if (asRawData && !expectsRawData) + { + if (ModContentManager.EnablePyTkLegacyMode) + { + if (!ModContentManager.IsPyTkScaled.TryGetValue(file.FullName, out bool isScaled)) + { + string? dirPath = file.DirectoryName; + string fileName = $"{Path.GetFileNameWithoutExtension(file.Name)}.pytk.json"; + + string path = dirPath is not null + ? Path.Combine(dirPath, fileName) + : fileName; + + ModContentManager.IsPyTkScaled[file.FullName] = isScaled = File.Exists(path); + } + + asRawData = !isScaled; + if (!asRawData) + this.Monitor.LogOnce("Enabled compatibility mode for PyTK scaled textures. This won't cause any issues, but may impact performance.", LogLevel.Warn); + } + else + asRawData = true; + } // load - if (asRawData || this.UseRawImageLoading) + if (asRawData) { - IRawTextureData raw = this.LoadRawImageData(file, asRawData); + IRawTextureData raw = this.LoadRawImageData(file, expectsRawData); - if (asRawData) + if (expectsRawData) return (T)raw; else { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index fa3f8778..7792c64a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1636,6 +1636,12 @@ namespace StardewModdingAPI.Framework // initialize translations this.ReloadTranslations(loaded); + // set temporary PyTK compatibility mode + { + IModInfo? pyTk = this.ModRegistry.Get("Platonymous.Toolkit"); + ModContentManager.EnablePyTkLegacyMode = pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.23.1"); + } + // initialize loaded non-content-pack mods this.Monitor.Log("Launching mods...", LogLevel.Debug); #pragma warning disable CS0612, CS0618 // deprecated code -- cgit