From 4a1055e573e9d8b0aa654238889596be07c29193 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Tue, 16 Aug 2022 15:30:21 -0400 Subject: arraypool in the modcontentmanager, a bit of fussing --- .../Framework/ContentManagers/ModContentManager.cs | 40 ++++++++++++++-------- 1 file changed, 25 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index cc6f8372..dd30c225 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,9 +1,11 @@ using System; +using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using BmFont; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; @@ -111,7 +113,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."); + this.ThrowLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager."); assetName = relativePath; } } @@ -123,7 +125,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get file FileInfo file = this.GetModFile(assetName.Name); if (!file.Exists) - throw this.GetLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist."); + this.ThrowLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist."); // load content asset = file.Extension.ToLower() switch @@ -141,7 +143,8 @@ namespace StardewModdingAPI.Framework.ContentManagers if (ex is SContentLoadException) throw; - throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex); + this.ThrowLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex); + return default; } // track & return asset @@ -189,7 +192,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, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method + this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method return asset; } @@ -301,7 +304,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."); + this.ThrowLoadError(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. @@ -326,7 +329,8 @@ 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'."); + this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); + return default; } /// Assert that the asset type is compatible with one of the allowed types. @@ -338,18 +342,20 @@ namespace StardewModdingAPI.Framework.ContentManagers 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))}'."); + this.ThrowLoadError(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. + /// Throws 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. + [DoesNotReturn] [DebuggerStepThrough, DebuggerHidden] - private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { - return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); + throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); } /// Get a file from the mod folder. @@ -384,12 +390,14 @@ namespace StardewModdingAPI.Framework.ContentManagers private Texture2D PremultiplyTransparency(Texture2D texture) { // premultiply pixels - Color[] data = GC.AllocateUninitializedArray(texture.Width * texture.Height); - texture.GetData(data); + int count = texture.Width * texture.Height; + Color[] data = ArrayPool.Shared.Rent(count); + texture.GetData(data, 0, count); + bool changed = false; - for (int i = 0; i < data.Length; i++) + for (int i = 0; i < count; i++) { - Color pixel = data[i]; + ref Color pixel = ref data[i]; if (pixel.A is (byte.MinValue or byte.MaxValue)) continue; // no need to change fully transparent/opaque pixels @@ -398,8 +406,10 @@ namespace StardewModdingAPI.Framework.ContentManagers } if (changed) - texture.SetData(data); + texture.SetData(data, 0, count); + // return + ArrayPool.Shared.Return(data); return texture; } -- cgit From 0a2a1a08def3d97f21b4138d9e39c563389b492a Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:30:20 -0400 Subject: Favor record structs when there are four or fewer elements. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 41 +++++++++++++++------- src/SMAPI/Framework/Content/AssetEditOperation.cs | 2 +- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 2 +- .../ContentManagers/GameContentManager.cs | 4 +-- 4 files changed, 33 insertions(+), 16 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index ea04f57a..5016bcd4 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework.Content this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); // get the pixels for the source area - Color[] sourceData; + Color[] trimmedSourceData; { int areaX = sourceArea.Value.X; int areaY = sourceArea.Value.Y; @@ -49,26 +49,42 @@ namespace StardewModdingAPI.Framework.Content if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height) { - sourceData = source.Data; - this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + trimmedSourceData = source.Data; + this.PatchImageImpl(trimmedSourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); } else { int pixelCount = areaWidth * areaHeight; - sourceData = ArrayPool.Shared.Rent(pixelCount); + trimmedSourceData = ArrayPool.Shared.Rent(pixelCount); - for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) + // shortcut! If I want a horizontal slice of the texture + // I can copy the whole array in one pass + // Likely ~uncommon but Array.Copy significantly benefits + // from being able to do this. + if (areaWidth == source.Width && areaX == 0) { - int sourceIndex = (y * source.Width) + areaX; - int targetIndex = (y - areaY) * areaWidth; - Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); + int sourceIndex = areaY * source.Width; + int targetIndex = 0; + + Array.Copy(source.Data, sourceIndex, trimmedSourceData, targetIndex, pixelCount); + } + else + { + // copying line-by-line + // Array.Copy isn't great at small scale + for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) + { + int sourceIndex = (y * source.Width) + areaX; + int targetIndex = (y - areaY) * areaWidth; + Array.Copy(source.Data, sourceIndex, trimmedSourceData, targetIndex, areaWidth); + } } // apply - this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + this.PatchImageImpl(trimmedSourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); // return - ArrayPool.Shared.Return(sourceData); + ArrayPool.Shared.Return(trimmedSourceData); } } } @@ -160,8 +176,9 @@ namespace StardewModdingAPI.Framework.Content // merge pixels for (int i = 0; i < pixelCount; i++) { - Color above = sourceData[i]; - Color below = mergedData[i]; + // should probably benchmark this... + ref Color above = ref sourceData[i]; + ref Color below = ref mergedData[i]; // shortcut transparency if (above.A < MinOpacity) diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs index 11b8811b..893f59bd 100644 --- a/src/SMAPI/Framework/Content/AssetEditOperation.cs +++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs @@ -8,5 +8,5 @@ namespace StardewModdingAPI.Framework.Content /// If there are multiple edits that apply to the same asset, the priority with which this one should be applied. /// The content pack on whose behalf the edit is being applied, if any. /// Apply the edit to an asset. - internal record AssetEditOperation(IModMetadata Mod, AssetEditPriority Priority, IModMetadata? OnBehalfOf, Action ApplyEdit); + internal readonly record struct AssetEditOperation(IModMetadata Mod, AssetEditPriority Priority, IModMetadata? OnBehalfOf, Action ApplyEdit); } diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index 7af07dfd..58886849 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -8,5 +8,5 @@ namespace StardewModdingAPI.Framework.Content /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - internal record AssetLoadOperation(IModMetadata Mod, IModMetadata? OnBehalfOf, AssetLoadPriority Priority, Func GetData); + internal readonly record struct AssetLoadOperation(IModMetadata Mod, IModMetadata? OnBehalfOf, AssetLoadPriority Priority, Func GetData); } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index df7bdc59..a8c70356 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -172,7 +172,7 @@ namespace StardewModdingAPI.Framework.ContentManagers where T : notnull { // find matching loader - AssetLoadOperation? loader = null; + AssetLoadOperation loader = default; if (loadOperations?.Count > 0) { if (!this.AssertMaxOneRequiredLoader(info, loadOperations, out string? error)) @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.ContentManagers loader = loadOperations.OrderByDescending(p => p.Priority).FirstOrDefault(); } - if (loader == null) + if (loader.Mod == null) // aka, this is default. return null; // fetch asset from loader -- cgit From 627100509c0a9c637b495473ef71f13093e885d2 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Wed, 17 Aug 2022 21:11:47 -0400 Subject: hide throwhelper from stack trace in dotnet 6 --- 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 dd30c225..0c793808 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -353,6 +353,9 @@ namespace StardewModdingAPI.Framework.ContentManagers [DoesNotReturn] [DebuggerStepThrough, DebuggerHidden] [MethodImpl(MethodImplOptions.NoInlining)] +#if NET6_0_OR_GREATER + [StackTraceHidden] +#endif private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); -- cgit From d29c01b8155a6fe2607066da6f0a5cfb45b28d3c Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Thu, 18 Aug 2022 20:51:45 -0400 Subject: Partially revert "Favor record structs when there are four or fewer elements." This reverts commit f5d49515c4eddfb415903a89d70654cf9b6de299. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 36 ++++++++++------------ src/SMAPI/Framework/Content/AssetEditOperation.cs | 2 +- src/SMAPI/Framework/Content/AssetLoadOperation.cs | 2 +- .../ContentManagers/GameContentManager.cs | 4 +-- 4 files changed, 21 insertions(+), 23 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 5016bcd4..5f175217 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework.Content this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); // get the pixels for the source area - Color[] trimmedSourceData; + Color[] sourceData; { int areaX = sourceArea.Value.X; int areaY = sourceArea.Value.Y; @@ -49,42 +49,40 @@ namespace StardewModdingAPI.Framework.Content if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height) { - trimmedSourceData = source.Data; - this.PatchImageImpl(trimmedSourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + sourceData = source.Data; + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); } else { int pixelCount = areaWidth * areaHeight; - trimmedSourceData = ArrayPool.Shared.Rent(pixelCount); + sourceData = ArrayPool.Shared.Rent(pixelCount); - // shortcut! If I want a horizontal slice of the texture - // I can copy the whole array in one pass - // Likely ~uncommon but Array.Copy significantly benefits - // from being able to do this. - if (areaWidth == source.Width && areaX == 0) + if (areaX == 0 && areaWidth == source.Width) { - int sourceIndex = areaY * source.Width; + // shortcut copying because the area to copy is contiguous. This is + // probably uncommon, but Array.Copy benefits a lot. + + int sourceIndex = areaY * areaWidth; int targetIndex = 0; + Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); - Array.Copy(source.Data, sourceIndex, trimmedSourceData, targetIndex, pixelCount); } else { - // copying line-by-line - // Array.Copy isn't great at small scale + // slower copying, line by line for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) { int sourceIndex = (y * source.Width) + areaX; int targetIndex = (y - areaY) * areaWidth; - Array.Copy(source.Data, sourceIndex, trimmedSourceData, targetIndex, areaWidth); + Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); } } // apply - this.PatchImageImpl(trimmedSourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); // return - ArrayPool.Shared.Return(trimmedSourceData); + ArrayPool.Shared.Return(sourceData); } } } @@ -176,9 +174,9 @@ namespace StardewModdingAPI.Framework.Content // merge pixels for (int i = 0; i < pixelCount; i++) { - // should probably benchmark this... - ref Color above = ref sourceData[i]; - ref Color below = ref mergedData[i]; + // ref locals here? Not sure. + Color above = sourceData[i]; + Color below = mergedData[i]; // shortcut transparency if (above.A < MinOpacity) diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs index 893f59bd..11b8811b 100644 --- a/src/SMAPI/Framework/Content/AssetEditOperation.cs +++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs @@ -8,5 +8,5 @@ namespace StardewModdingAPI.Framework.Content /// If there are multiple edits that apply to the same asset, the priority with which this one should be applied. /// The content pack on whose behalf the edit is being applied, if any. /// Apply the edit to an asset. - internal readonly record struct AssetEditOperation(IModMetadata Mod, AssetEditPriority Priority, IModMetadata? OnBehalfOf, Action ApplyEdit); + internal record AssetEditOperation(IModMetadata Mod, AssetEditPriority Priority, IModMetadata? OnBehalfOf, Action ApplyEdit); } diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs index 58886849..7af07dfd 100644 --- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs +++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs @@ -8,5 +8,5 @@ namespace StardewModdingAPI.Framework.Content /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. /// The content pack on whose behalf the asset is being loaded, if any. /// Load the initial value for an asset. - internal readonly record struct AssetLoadOperation(IModMetadata Mod, IModMetadata? OnBehalfOf, AssetLoadPriority Priority, Func GetData); + internal record AssetLoadOperation(IModMetadata Mod, IModMetadata? OnBehalfOf, AssetLoadPriority Priority, Func GetData); } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index a8c70356..df7bdc59 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -172,7 +172,7 @@ namespace StardewModdingAPI.Framework.ContentManagers where T : notnull { // find matching loader - AssetLoadOperation loader = default; + AssetLoadOperation? loader = null; if (loadOperations?.Count > 0) { if (!this.AssertMaxOneRequiredLoader(info, loadOperations, out string? error)) @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.ContentManagers loader = loadOperations.OrderByDescending(p => p.Priority).FirstOrDefault(); } - if (loader.Mod == null) // aka, this is default. + if (loader == null) return null; // fetch asset from loader -- cgit From 40d5cd7c05d4e0a4e6894cd7b9f6d7d747716837 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 17:42:32 -0400 Subject: use try..finally to make sure rented arrays are returned --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 108 +++++++++++---------- .../Framework/ContentManagers/ModContentManager.cs | 35 ++++--- 2 files changed, 79 insertions(+), 64 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 636d4a71..3abcd328 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -64,20 +63,23 @@ namespace StardewModdingAPI.Framework.Content { int pixelCount = areaWidth * areaHeight; sourceData = ArrayPool.Shared.Rent(pixelCount); - - // slower copying, line by line - for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) + try { - int sourceIndex = (y * source.Width) + areaX; - int targetIndex = (y - areaY) * areaWidth; - Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); + // slower copying, line by line + for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) + { + int sourceIndex = (y * source.Width) + areaX; + int targetIndex = (y - areaY) * areaWidth; + Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); + } + + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + finally + { + ArrayPool.Shared.Return(sourceData); } - - // apply - this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); - - // return - ArrayPool.Shared.Return(sourceData); } } } @@ -98,13 +100,15 @@ namespace StardewModdingAPI.Framework.Content // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = ArrayPool.Shared.Rent(pixelCount); - source.GetData(0, sourceArea, sourceData, 0, pixelCount); - - // apply - this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); - - // return - ArrayPool.Shared.Return(sourceData); + try + { + source.GetData(0, sourceArea, sourceData, 0, pixelCount); + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + finally + { + ArrayPool.Shared.Return(sourceData); + } } /// @@ -207,41 +211,47 @@ namespace StardewModdingAPI.Framework.Content // get target data Color[] mergedData = ArrayPool.Shared.Rent(pixelCount); - target.GetData(0, targetArea, mergedData, 0, pixelCount); - - // merge pixels - for (int i = startIndex; i <= endIndex; i++) + try { - int targetIndex = i - sourceoffset; - - // ref locals here? Not sure. - Color above = sourceData[i]; - Color below = mergedData[targetIndex]; - - // shortcut transparency - if (above.A < MinOpacity) - continue; - if (below.A < MinOpacity || above.A == byte.MaxValue) - mergedData[targetIndex] = above; + target.GetData(0, targetArea, mergedData, 0, pixelCount); // merge pixels - else + for (int i = startIndex; i <= endIndex; i++) { - // 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[targetIndex] = 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) - ); + int targetIndex = i - sourceoffset; + + // ref locals here? Not sure. + Color above = sourceData[i]; + Color below = mergedData[targetIndex]; + + // shortcut transparency + if (above.A < MinOpacity) + continue; + if (below.A < MinOpacity || above.A == byte.MaxValue) + mergedData[targetIndex] = above; + + // merge pixels + 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[targetIndex] = 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) + ); + } } - } - target.SetData(0, targetArea, mergedData, 0, pixelCount); - ArrayPool.Shared.Return(mergedData); + target.SetData(0, targetArea, mergedData, 0, pixelCount); + } + finally + { + ArrayPool.Shared.Return(mergedData); + } } } } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 0c793808..6b8a5874 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -395,25 +395,30 @@ namespace StardewModdingAPI.Framework.ContentManagers // premultiply pixels int count = texture.Width * texture.Height; Color[] data = ArrayPool.Shared.Rent(count); - texture.GetData(data, 0, count); - - bool changed = false; - for (int i = 0; i < count; i++) + try { - ref Color pixel = ref data[i]; - if (pixel.A is (byte.MinValue or byte.MaxValue)) - continue; // no need to change fully transparent/opaque pixels + texture.GetData(data, 0, count); - 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; - } + bool changed = false; + for (int i = 0; i < count; i++) + { + ref Color pixel = ref data[i]; + if (pixel.A is (byte.MinValue or byte.MaxValue)) + continue; // no need to change fully transparent/opaque pixels - if (changed) - texture.SetData(data, 0, count); + 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; + } - // return - ArrayPool.Shared.Return(data); - return texture; + if (changed) + texture.SetData(data, 0, count); + + return texture; + } + finally + { + ArrayPool.Shared.Return(data); + } } /// Fix custom map tilesheet paths so they can be found by the content manager. -- cgit From 2e0bc5ddfe90102fe5adbc90b2d53c5cbb8405fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 17:45:50 -0400 Subject: tweak new code --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 183 ++++++++++----------- .../Framework/ContentManagers/ModContentManager.cs | 13 +- src/SMAPI/Framework/Logging/LogOnceCacheKey.cs | 10 ++ src/SMAPI/Framework/Monitor.cs | 14 +- 4 files changed, 104 insertions(+), 116 deletions(-) create mode 100644 src/SMAPI/Framework/Logging/LogOnceCacheKey.cs (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 3abcd328..0380dd9e 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -33,71 +33,59 @@ namespace StardewModdingAPI.Framework.Content /// public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - // nullcheck if (source == null) throw new ArgumentNullException(nameof(source), "Can't patch from null source data."); + // get normalized bounds this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); - - // check to see if the Data is sufficiently long. - // while SMAPI's impl is going to be, it's not necessarily the case for mod impl. if (source.Data.Length < (sourceArea.Value.Bottom - 1) * source.Width + sourceArea.Value.Right) - throw new ArgumentException("Source data insufficiently long for this operation."); - - // get the pixels for the source area - Color[] sourceData; + throw new ArgumentException("Can't apply image patch because the source image is smaller than the source area.", nameof(source)); + int areaX = sourceArea.Value.X; + int areaY = sourceArea.Value.Y; + int areaWidth = sourceArea.Value.Width; + int areaHeight = sourceArea.Value.Height; + + // shortcut: if the area width matches the source image, we can apply the image as-is without needing + // to copy the pixels into a smaller subset. It's fine if the source is taller than the area, since we'll + // just ignore the extra data at the end of the pixel array. + if (areaWidth == source.Width) { - int areaX = sourceArea.Value.X; - int areaY = sourceArea.Value.Y; - int areaWidth = sourceArea.Value.Width; - int areaHeight = sourceArea.Value.Height; + this.PatchImageImpl(source.Data, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY); + return; + } - if (areaWidth == source.Width) + // else copy the pixels within the smaller area & apply that + int pixelCount = areaWidth * areaHeight; + Color[] sourceData = ArrayPool.Shared.Rent(pixelCount); + try + { + for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) { - // It's actually fine if the source is taller than the sourceArea - // the "extra" bits on the end of the array can just be ignored. - sourceData = source.Data; - this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY); + int sourceIndex = (y * source.Width) + areaX; + int targetIndex = (y - areaY) * areaWidth; + Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); } - else - { - int pixelCount = areaWidth * areaHeight; - sourceData = ArrayPool.Shared.Rent(pixelCount); - try - { - // slower copying, line by line - for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) - { - int sourceIndex = (y * source.Width) + areaX; - int targetIndex = (y - areaY) * areaWidth; - Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); - } - // apply - this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); - } - finally - { - ArrayPool.Shared.Return(sourceData); - } - } + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + finally + { + ArrayPool.Shared.Return(sourceData); } } /// public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - // nullcheck if (source == null) throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); + // get normalized bounds this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); - - // validate source bounds if (!source.Bounds.Contains(sourceArea.Value)) throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - // get source data + // get source data & apply int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = ArrayPool.Shared.Rent(pixelCount); try @@ -164,94 +152,91 @@ namespace StardewModdingAPI.Framework.Content if (sourceArea.Size != targetArea.Size) throw new InvalidOperationException("The source and target areas must be the same size."); + // shortcut: replace the entire area if (patchMode == PatchMode.Replace) - target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount); - else { - // merge data - - // Content packs have a habit of using large amounts of blank space. - // Adjusting bounds to ignore transparent pixels at the start and end. + target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount); + return; + } - int startIndex = -1; + // skip transparent pixels at the start & end (e.g. large spritesheet with a few sprites replaced) + int startIndex = -1; + int endIndex = -1; + { for (int i = startRow * sourceArea.Width; i < pixelCount; i++) { - if (sourceData[i].A >= MinOpacity) + if (sourceData[i].A >= AssetDataForImage.MinOpacity) { startIndex = i; break; } } - if (startIndex == -1) - return; // apparently a completely blank texture? + return; // blank texture - int endIndex = -1; for (int i = startRow * sourceArea.Width + pixelCount - 1; i >= startIndex; i--) { - if (sourceData[i].A >= MinOpacity) + if (sourceData[i].A >= AssetDataForImage.MinOpacity) { endIndex = i; break; } } - if (endIndex == -1) - return; // should never happen + return; // ??? + } - // Calculate new Y bounds - int topoffset = startIndex / sourceArea.Width; - int bottomoffset = endIndex / sourceArea.Width; + // update target rectangle + int sourceOffset; + { + int topOffset = startIndex / sourceArea.Width; + int bottomOffset = endIndex / sourceArea.Width; - // Update target rectangle - targetArea = new(targetArea.X, targetArea.Y + topoffset, targetArea.Width, bottomoffset - topoffset + 1); + targetArea = new(targetArea.X, targetArea.Y + topOffset, targetArea.Width, bottomOffset - topOffset + 1); pixelCount = targetArea.Width * targetArea.Height; + sourceOffset = topOffset * sourceArea.Width; + } - int sourceoffset = topoffset * sourceArea.Width; + // apply + Color[] mergedData = ArrayPool.Shared.Rent(pixelCount); + try + { + target.GetData(0, targetArea, mergedData, 0, pixelCount); - // get target data - Color[] mergedData = ArrayPool.Shared.Rent(pixelCount); - try + for (int i = startIndex; i <= endIndex; i++) { - target.GetData(0, targetArea, mergedData, 0, pixelCount); + int targetIndex = i - sourceOffset; - // merge pixels - for (int i = startIndex; i <= endIndex; i++) - { - int targetIndex = i - sourceoffset; + Color above = sourceData[i]; + Color below = mergedData[targetIndex]; - // ref locals here? Not sure. - Color above = sourceData[i]; - Color below = mergedData[targetIndex]; + // shortcut transparency + if (above.A < AssetDataForImage.MinOpacity) + continue; + if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue) + mergedData[targetIndex] = above; - // shortcut transparency - if (above.A < MinOpacity) - continue; - if (below.A < MinOpacity || above.A == byte.MaxValue) - mergedData[targetIndex] = above; - - // merge pixels - 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[targetIndex] = 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) - ); - } + // merge pixels + 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[targetIndex] = 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) + ); } - - target.SetData(0, targetArea, mergedData, 0, pixelCount); - } - finally - { - ArrayPool.Shared.Return(mergedData); } + + target.SetData(0, targetArea, mergedData, 0, pixelCount); + } + finally + { + ArrayPool.Shared.Return(mergedData); } } } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 6b8a5874..72dcf6e1 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -241,7 +241,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { using FileStream stream = File.OpenRead(file.FullName); Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); + this.PremultiplyTransparency(texture); return (T)(object)texture; } } @@ -345,17 +345,15 @@ namespace StardewModdingAPI.Framework.ContentManagers this.ThrowLoadError(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))}'."); } - /// Throws an error which indicates that an asset couldn't be loaded. + /// Throw 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. + /// [DoesNotReturn] [DebuggerStepThrough, DebuggerHidden] [MethodImpl(MethodImplOptions.NoInlining)] -#if NET6_0_OR_GREATER - [StackTraceHidden] -#endif private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); @@ -390,9 +388,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The texture to premultiply. /// Returns a premultiplied texture. /// Based on code by David Gouveia. - private Texture2D PremultiplyTransparency(Texture2D texture) + private void PremultiplyTransparency(Texture2D texture) { - // premultiply pixels int count = texture.Width * texture.Height; Color[] data = ArrayPool.Shared.Rent(count); try @@ -412,8 +409,6 @@ namespace StardewModdingAPI.Framework.ContentManagers if (changed) texture.SetData(data, 0, count); - - return texture; } finally { diff --git a/src/SMAPI/Framework/Logging/LogOnceCacheKey.cs b/src/SMAPI/Framework/Logging/LogOnceCacheKey.cs new file mode 100644 index 00000000..4d31ffeb --- /dev/null +++ b/src/SMAPI/Framework/Logging/LogOnceCacheKey.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StardewModdingAPI.Framework.Logging +{ + /// The cache key for the . + /// The log message. + /// The log level. + [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local", Justification = "This is only used as a lookup key.")] + internal readonly record struct LogOnceCacheKey(string Message, LogLevel Level); +} diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index d33bf259..4ed2c9bb 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -25,15 +25,13 @@ namespace StardewModdingAPI.Framework private readonly LogFileManager LogFile; /// The maximum length of the values. - private static readonly int MaxLevelLength = (from level in Enum.GetValues() select level.ToString().Length).Max(); + private static readonly int MaxLevelLength = Enum.GetValues().Max(level => level.ToString().Length); - /// A mapping of console log levels to their string form. - private static readonly Dictionary LogStrings = Enum.GetValues().ToDictionary(k => k, v => v.ToString().ToUpper().PadRight(MaxLevelLength)); - - private readonly record struct LogOnceCacheEntry(string message, LogLevel level); + /// The cached representation for each level when added to a log header. + private static readonly Dictionary LogStrings = Enum.GetValues().ToDictionary(level => level, level => level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength)); /// A cache of messages that should only be logged once. - private readonly HashSet LogOnceCache = new(); + private readonly HashSet LogOnceCache = new(); /// Get the screen ID that should be logged to distinguish between players in split-screen mode, if any. private readonly Func GetScreenIdForLog; @@ -89,7 +87,7 @@ namespace StardewModdingAPI.Framework /// public void LogOnce(string message, LogLevel level = LogLevel.Trace) { - if (this.LogOnceCache.Add(new LogOnceCacheEntry(message, level))) + if (this.LogOnceCache.Add(new LogOnceCacheKey(message, level))) this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } @@ -152,7 +150,7 @@ namespace StardewModdingAPI.Framework /// The log level. private string GenerateMessagePrefix(string source, ConsoleLogLevel level) { - string levelStr = LogStrings[level]; + string levelStr = Monitor.LogStrings[level]; int? playerIndex = this.GetScreenIdForLog(); return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]"; -- cgit From 037d7e357b169936fa858f6c8b9c1388ca75840b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 17:39:11 -0400 Subject: set texture name earlier to support mods like SpriteMaster --- docs/release-notes.md | 1 + src/SMAPI/Framework/Content/AssetDataForImage.cs | 2 +- .../ContentManagers/BaseContentManager.cs | 2 +- .../Framework/ContentManagers/ModContentManager.cs | 4 +-- src/SMAPI/Framework/InternalExtensions.cs | 30 ++++++++++++++++++++++ 5 files changed, 35 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bf3b875..da3db590 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * When [providing a mod API for a C# mod](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations), you can now get an optional parameter with the mod requesting the API (thanks to KhloeLeclair!). This avoids needing the pattern where each method needs the requesting mod's manifest. * SMAPI now treats square brackets in the manifest `Name` field as round ones to avoid breaking tools which parse log files. * Made deprecation message wording stronger for the upcoming SMAPI 4.0.0 release. + * The `Texture2D.Name` field is now set earlier to support mods like SpriteMaster. * Updated dependencies: [Harmony](https://harmony.pardeike.net) 2.2.2 (see [changes](https://github.com/pardeike/Harmony/releases/tag/v2.2.2.0)) and [FluentHttpClient](https://github.com/Pathoschild/FluentHttpClient#readme) 4.2.0 (see [changes](https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#420)). * Fixed `LocationListChanged` event not raised & memory leak occurring when a generated mine/volcano is removed (thanks to tylergibbs2!). diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 0380dd9e..241c09a8 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -106,7 +106,7 @@ namespace StardewModdingAPI.Framework.Content return false; Texture2D original = this.Data; - Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); + Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)).SetName(original.Name); this.ReplaceWith(texture); this.PatchImage(original); return true; diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 54f8e2a2..1e9f4ffe 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -336,7 +336,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // track asset key if (value is Texture2D texture) - texture.Name = assetName.Name; + texture.SetName(assetName); // save to cache // Note: even if the asset was loaded and cached right before this method was called, diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 72dcf6e1..30dd4215 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -232,7 +232,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return (T)raw; else { - Texture2D texture = new(Game1.graphics.GraphicsDevice, raw.Width, raw.Height); + Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, raw.Width, raw.Height).SetName(assetName); texture.SetData(raw.Data); return (T)(object)texture; } @@ -240,7 +240,7 @@ namespace StardewModdingAPI.Framework.ContentManagers else { using FileStream stream = File.OpenRead(file.FullName); - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream).SetName(assetName); this.PremultiplyTransparency(texture); return (T)(object)texture; } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index ba9bbcec..7ad30d35 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.Xna.Framework.Graphics; @@ -163,5 +164,34 @@ namespace StardewModdingAPI.Framework { return reflection.GetField(spriteBatch, "_beginCalled").GetValue(); } + + /**** + ** Texture2D + ****/ + /// Set the texture name field. + /// The texture whose name to set. + /// The asset name to set. + /// Returns the texture for chaining. + [return: NotNullIfNotNull("texture")] + public static Texture2D? SetName(this Texture2D? texture, IAssetName assetName) + { + if (texture != null) + texture.Name = assetName.Name; + + return texture; + } + + /// Set the texture name field. + /// The texture whose name to set. + /// The asset name to set. + /// Returns the texture for chaining. + [return: NotNullIfNotNull("texture")] + public static Texture2D? SetName(this Texture2D? texture, string assetName) + { + if (texture != null) + texture.Name = assetName; + + return texture; + } } } -- cgit From b78b269cf53529bcb73fa99538a3e9eb23e283b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 17:56:33 -0400 Subject: split PyTK raw-image-load check into a separate method so it can be patched by mods like SpriteMaster --- .../Framework/ContentManagers/ModContentManager.cs | 51 ++++++++++++++-------- 1 file changed, 32 insertions(+), 19 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 30dd4215..ddb6f6f5 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -204,31 +204,22 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadImageFile(IAssetName assetName, FileInfo file) { this.AssertValidType(assetName, file, typeof(Texture2D), typeof(IRawTextureData)); - bool expectsRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); - bool asRawData = expectsRawData || this.UseRawImageLoading; - + bool returnRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); + bool loadRawData = + returnRawData + || ( + this.UseRawImageLoading #if SMAPI_DEPRECATED - // disable raw data if PyTK will rescale the image (until it supports raw data) - if (asRawData && !expectsRawData) - { - if (ModContentManager.EnablePyTkLegacyMode) - { - // PyTK intercepts Texture2D file loads to rescale them (e.g. for HD portraits), - // but doesn't support IRawTextureData loads yet. We can't just check if the - // current file has a '.pytk.json' rescale file though, since PyTK may still - // rescale it if the original asset or another edit gets rescaled. - asRawData = false; - this.Monitor.LogOnce("Enabled compatibility mode for PyTK 1.23.* or earlier. This won't cause any issues, but may impact performance. This will no longer be supported in the upcoming SMAPI 4.0.0.", LogLevel.Warn); - } - } + && !this.ShouldDisableIntermediateRawDataLoad(assetName, file) #endif + ); // load - if (asRawData) + if (loadRawData) { - IRawTextureData raw = this.LoadRawImageData(file, expectsRawData); + IRawTextureData raw = this.LoadRawImageData(file, returnRawData); - if (expectsRawData) + if (returnRawData) return (T)raw; else { @@ -246,6 +237,28 @@ namespace StardewModdingAPI.Framework.ContentManagers } } +#if SMAPI_DEPRECATED + /// Get whether to disable loading an image as before building a instance. This isn't called if the mod requested directly. + /// The type of asset being loaded. + /// The asset name relative to the loader root directory. + /// The file being loaded. + private bool ShouldDisableIntermediateRawDataLoad(IAssetName assetName, FileInfo file) + { + // disable raw data if PyTK will rescale the image (until it supports raw data) + if (ModContentManager.EnablePyTkLegacyMode) + { + // PyTK intercepts Texture2D file loads to rescale them (e.g. for HD portraits), + // but doesn't support IRawTextureData loads yet. We can't just check if the + // current file has a '.pytk.json' rescale file though, since PyTK may still + // rescale it if the original asset or another edit gets rescaled. + this.Monitor.LogOnce("Enabled compatibility mode for PyTK 1.23.* or earlier. This won't cause any issues, but may impact performance. This will no longer be supported in the upcoming SMAPI 4.0.0.", LogLevel.Warn); + return true; + } + + return false; + } +#endif + /// Load the raw image data from a file on disk. /// The file whose data to load. /// Whether the data is being loaded for an (true) or (false) instance. -- cgit From 27856ebea25791d2d82a42a670327f6dd5c4ac23 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 18:03:05 -0400 Subject: drop UseRawImageLoading option Raw image loading is now always enabled, except in PyTK compatibility mode. --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 10 ++---- .../Framework/ContentManagers/ModContentManager.cs | 42 ++++++++-------------- src/SMAPI/Framework/Models/SConfig.cs | 8 +---- src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/SMAPI.config.json | 6 ---- 6 files changed, 19 insertions(+), 51 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/docs/release-notes.md b/docs/release-notes.md index da3db590..b5a0adaf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ * Optimized performance and memory usage (thanks to atravita!). * Other internal optimizations. * Added more file extensions to ignore when searching for mod folders: `.7z`, `.tar`, `.tar.gz`, and `.xcf` (thanks to atravita!). + * Removed `UseRawImageLoading` option. This is now always enabled, except when PyTK is installed. * Fixed update alerts incorrectly shown for prerelease versions on GitHub that aren't marked as prerelease. * For mod authors: diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 9e044b44..cf26307f 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -34,9 +34,6 @@ namespace StardewModdingAPI.Framework /// An asset key prefix for assets from SMAPI mod folders. private readonly string ManagedPrefix = "SMAPI"; - /// 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; @@ -139,8 +136,7 @@ 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 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) + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations) { this.GetFileLookup = getFileLookup; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); @@ -151,7 +147,6 @@ namespace StardewModdingAPI.Framework this.OnAssetsInvalidated = onAssetsInvalidated; this.RequestAssetOperations = requestAssetOperations; this.FullRootDirectory = Path.Combine(Constants.GamePath, rootDirectory); - this.UseRawImageLoading = useRawImageLoading; this.ContentManagers.Add( this.MainContentManager = new GameContentManager( name: "Game1.content", @@ -230,8 +225,7 @@ namespace StardewModdingAPI.Framework reflection: this.Reflection, jsonHelper: this.JsonHelper, onDisposing: this.OnDisposing, - fileLookup: this.GetFileLookup(rootDirectory), - useRawImageLoading: this.UseRawImageLoading + fileLookup: this.GetFileLookup(rootDirectory) ); this.ContentManagers.Add(manager); return manager; diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index ddb6f6f5..badbd766 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -30,9 +30,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Fields *********/ - /// 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; @@ -74,15 +71,13 @@ 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 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) + 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) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; this.JsonHelper = jsonHelper; this.ModName = modName; - this.UseRawImageLoading = useRawImageLoading; this.TryLocalizeKeys = false; } @@ -205,34 +200,25 @@ namespace StardewModdingAPI.Framework.ContentManagers { this.AssertValidType(assetName, file, typeof(Texture2D), typeof(IRawTextureData)); bool returnRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); - bool loadRawData = - returnRawData - || ( - this.UseRawImageLoading + #if SMAPI_DEPRECATED - && !this.ShouldDisableIntermediateRawDataLoad(assetName, file) + if (!returnRawData && this.ShouldDisableIntermediateRawDataLoad(assetName, file)) + { + using FileStream stream = File.OpenRead(file.FullName); + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream).SetName(assetName); + this.PremultiplyTransparency(texture); + return (T)(object)texture; + } #endif - ); - // load - if (loadRawData) - { - IRawTextureData raw = this.LoadRawImageData(file, returnRawData); + IRawTextureData raw = this.LoadRawImageData(file, returnRawData); - if (returnRawData) - return (T)raw; - else - { - Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, raw.Width, raw.Height).SetName(assetName); - texture.SetData(raw.Data); - return (T)(object)texture; - } - } + if (returnRawData) + return (T)raw; else { - using FileStream stream = File.OpenRead(file.FullName); - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream).SetName(assetName); - this.PremultiplyTransparency(texture); + Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, raw.Width, raw.Height).SetName(assetName); + texture.SetData(raw.Data); return (T)(object)texture; } } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b3061fba..bceb0940 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -22,7 +22,6 @@ namespace StardewModdingAPI.Framework.Models [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", [nameof(LogNetworkTraffic)] = false, [nameof(RewriteMods)] = true, - [nameof(UseRawImageLoading)] = true, [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux, [nameof(SuppressHarmonyDebugMode)] = true }; @@ -68,9 +67,6 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should rewrite mods for compatibility. public bool RewriteMods { get; set; } - /// Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU. - public bool UseRawImageLoading { get; set; } - /// Whether to make SMAPI file APIs case-insensitive, even on Linux. public bool UseCaseInsensitivePaths { get; set; } @@ -99,13 +95,12 @@ namespace StardewModdingAPI.Framework.Models /// The base URL for SMAPI's web API, used to perform update checks. /// 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 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. /// Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod. /// 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? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks) + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -115,7 +110,6 @@ namespace StardewModdingAPI.Framework.Models this.WebApiBaseUrl = webApiBaseUrl; this.VerboseLogging = new HashSet(verboseLogging ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)]; - 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 4d6deb15..40979b09 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1333,8 +1333,7 @@ namespace StardewModdingAPI.Framework onAssetLoaded: this.OnAssetLoaded, onAssetsInvalidated: this.OnAssetsInvalidated, getFileLookup: this.GetFileLookup, - requestAssetOperations: this.RequestAssetOperations, - useRawImageLoading: this.Settings.UseRawImageLoading + requestAssetOperations: this.RequestAssetOperations ); 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 2d4239ba..635e3add 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -54,12 +54,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "UseCaseInsensitivePaths": null, - /** - * Whether to use raw image data when possible, instead of initializing an XNA Texture2D - * instance through the GPU. - */ - "UseRawImageLoading": true, - /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as -- cgit